17-pretty-web-interface #1
 Merged
	
	
		
		
			
		
		
		
		
			
		
		
			
			
				root
				merged 30 commits from 17-pretty-web-interface  into master 3 years ago
			
		
	
	
				 146 changed files with 4659 additions and 2312 deletions
			
			
		@ -0,0 +1,6 @@ | 
				
			||||
[build] | 
				
			||||
rustflags = [ | 
				
			||||
    "-L", "/home/ortem/src/rust/unki/static/lib", | 
				
			||||
    "--remap-path-prefix=/home/ortem/src/rust/unki=src", | 
				
			||||
    "--remap-path-prefix=/home/ortem/.cargo=cargo" | 
				
			||||
] | 
				
			||||
@ -1,9 +1,15 @@ | 
				
			||||
target/ | 
				
			||||
**/*.rs.bk | 
				
			||||
.idea/ | 
				
			||||
data/ | 
				
			||||
certs/ | 
				
			||||
static/ | 
				
			||||
.vscode/ | 
				
			||||
release/ | 
				
			||||
**/node_modules/ | 
				
			||||
 | 
				
			||||
**/*.rs.bk | 
				
			||||
**/*.pyc | 
				
			||||
certs/* | 
				
			||||
*.log | 
				
			||||
echoer | 
				
			||||
.env.private | 
				
			||||
*.lock | 
				
			||||
@ -1,26 +0,0 @@ | 
				
			||||
.PHONY: _pre_build debug release run clean unit-tests integration-tests test | 
				
			||||
 | 
				
			||||
CARGO=./scripts/cargo_musl.sh
 | 
				
			||||
 | 
				
			||||
clean: | 
				
			||||
	${CARGO} clean
 | 
				
			||||
 | 
				
			||||
_pre_build: | 
				
			||||
	docker build -t unki/musllibs ./muslrust
 | 
				
			||||
 | 
				
			||||
debug: _pre_build | 
				
			||||
	${CARGO} build
 | 
				
			||||
 | 
				
			||||
release: _pre_build | 
				
			||||
	${CARGO} build --release
 | 
				
			||||
 | 
				
			||||
run: build | 
				
			||||
	${CARGO} run
 | 
				
			||||
 | 
				
			||||
unit-tests: | 
				
			||||
	${CARGO} test --lib
 | 
				
			||||
 | 
				
			||||
integration-tests: | 
				
			||||
	cd ./integration && ./integration_tests.sh
 | 
				
			||||
 | 
				
			||||
test: unit-tests integration-tests | 
				
			||||
@ -0,0 +1,99 @@ | 
				
			||||
# i need to preserve --release in args, not to pass cargo make -p release
 | 
				
			||||
# due to cargo failing to parse "" argument
 | 
				
			||||
env_scripts = ['''
 | 
				
			||||
#!@duckscript
 | 
				
			||||
args = array ${1} ${2} ${3} ${4} ${5} ${6} ${7}
 | 
				
			||||
set_env PROFILE_OVERRIDE debug | 
				
			||||
 | 
				
			||||
for arg in ${args} | 
				
			||||
    e = eq ${arg} "--release"
 | 
				
			||||
    if ${e}
 | 
				
			||||
        set_env PROFILE_OVERRIDE release
 | 
				
			||||
    end
 | 
				
			||||
end | 
				
			||||
 | 
				
			||||
profile = get_env PROFILE_OVERRIDE
 | 
				
			||||
echo PROFILE_OVERRIDE=${profile}
 | 
				
			||||
'''] | 
				
			||||
 | 
				
			||||
[config] | 
				
			||||
default_to_workspace = false
 | 
				
			||||
 | 
				
			||||
[env] | 
				
			||||
TARGET = "x86_64-unknown-linux-musl"
 | 
				
			||||
CARGO = "cargo"
 | 
				
			||||
ROOTDIR = "${CARGO_MAKE_WORKING_DIRECTORY}"
 | 
				
			||||
STATIC_PREFIX = "${ROOTDIR}/static"
 | 
				
			||||
PQ_LIB_STATIC_X86_64_UNKNOWN_LINUX_MUSL = "true"
 | 
				
			||||
PG_CONFIG_X86_64_UNKNOWN_LINUX_GNU = "${STATIC_PREFIX}/bin/pg_config"
 | 
				
			||||
OPENSSL_STATIC = "true"
 | 
				
			||||
OPENSSL_DIR = "${STATIC_PREFIX}"
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
[tasks.build_static_libs] | 
				
			||||
script = "./scripts/build_musl_libs.sh"
 | 
				
			||||
 | 
				
			||||
[tasks.build_frontend] | 
				
			||||
script = '''
 | 
				
			||||
cd ./bin/u_panel/src/server/fe | 
				
			||||
ng build | 
				
			||||
''' | 
				
			||||
 | 
				
			||||
[tasks.clean] | 
				
			||||
command = "${CARGO}"
 | 
				
			||||
args = ["clean"]
 | 
				
			||||
 | 
				
			||||
[tasks.cargo_build] | 
				
			||||
dependencies = ["build_static_libs", "build_frontend"]
 | 
				
			||||
command = "${CARGO}"
 | 
				
			||||
args = ["build", "--target", "${TARGET}", "${@}"]
 | 
				
			||||
 | 
				
			||||
[tasks.cargo_update] | 
				
			||||
command = "${CARGO}"
 | 
				
			||||
args = ["update"]
 | 
				
			||||
 | 
				
			||||
[tasks.release_tasks] | 
				
			||||
condition = { env = { PROFILE_OVERRIDE = "release"} }
 | 
				
			||||
script = '''
 | 
				
			||||
BINS=$(ls ./target/${TARGET}/${PROFILE_OVERRIDE}/u_* -1 | grep -v ".d")
 | 
				
			||||
echo "Stripping..." | 
				
			||||
strip $BINS | 
				
			||||
echo "Packing..." | 
				
			||||
upx -9 $BINS | 
				
			||||
''' | 
				
			||||
 | 
				
			||||
[tasks.build] | 
				
			||||
dependencies = ["cargo_update", "cargo_build", "release_tasks"]
 | 
				
			||||
clear = true
 | 
				
			||||
 | 
				
			||||
[tasks.run] | 
				
			||||
disabled = true
 | 
				
			||||
 | 
				
			||||
[tasks.unit] | 
				
			||||
command = "${CARGO}"
 | 
				
			||||
args = ["test", "--target", "${TARGET}", "--lib", "--", "${@}"]
 | 
				
			||||
 | 
				
			||||
[tasks.integration] | 
				
			||||
dependencies = ["cargo_update"]
 | 
				
			||||
script = '''
 | 
				
			||||
[[ ! -d "./target/${TARGET}/${PROFILE_OVERRIDE}" ]] && echo 'No target folder. Build project first' && exit 1 | 
				
			||||
cd ./integration | 
				
			||||
bash integration_tests.sh ${@} | 
				
			||||
''' | 
				
			||||
 | 
				
			||||
[tasks.gen_schema] | 
				
			||||
script = '''
 | 
				
			||||
docker run --rm \ | 
				
			||||
    --env-file=$PWD/.env \
 | 
				
			||||
    --env-file=$PWD/.env.private \
 | 
				
			||||
    -v $PWD:/unki \
 | 
				
			||||
    -w /unki \
 | 
				
			||||
    unki/u_db \
 | 
				
			||||
    /unki/images/integration-tests/u_db_entrypoint.sh || true
 | 
				
			||||
''' | 
				
			||||
 | 
				
			||||
[tasks.test] | 
				
			||||
dependencies = ["unit", "integration"]
 | 
				
			||||
 | 
				
			||||
[tasks.deploy] | 
				
			||||
script = './scripts/deploy.sh'
 | 
				
			||||
@ -1,88 +1,120 @@ | 
				
			||||
// TODO:
 | 
				
			||||
// поддержка питона
 | 
				
			||||
// резолв адреса управляющего сервера через DoT
 | 
				
			||||
// кроссплатформенность (реализовать интерфейс для винды и никсов)
 | 
				
			||||
// проверка обнов
 | 
				
			||||
// самоуничтожение
 | 
				
			||||
 | 
				
			||||
#[macro_use] | 
				
			||||
extern crate log; | 
				
			||||
extern crate env_logger; | 
				
			||||
 | 
				
			||||
use std::env; | 
				
			||||
//use daemonize::Daemonize;
 | 
				
			||||
use std::sync::Arc; | 
				
			||||
use tokio::time::{sleep, Duration}; | 
				
			||||
use u_lib::{ | 
				
			||||
    api::ClientHandler, | 
				
			||||
    builder::JobBuilder, | 
				
			||||
    cache::JobCache, | 
				
			||||
    executor::pop_completed, | 
				
			||||
    models::{AssignedJob, ExecResult}, | 
				
			||||
    UID, | 
				
			||||
    //daemonize
 | 
				
			||||
    api::ClientHandler, cache::JobCache, config::get_self_uid, errors::ErrChan, | 
				
			||||
    executor::pop_completed, logging::init_logger, messaging::Reportable, models::AssignedJobById, | 
				
			||||
    runner::JobRunner, utils::load_env_default, | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
#[macro_export] | 
				
			||||
macro_rules! retry_until_ok { | 
				
			||||
    ( $body:expr ) => { | 
				
			||||
        loop { | 
				
			||||
            match $body { | 
				
			||||
                Ok(r) => break r, | 
				
			||||
                Err(e) => error!("{:?}", e), | 
				
			||||
            }; | 
				
			||||
            sleep(Duration::from_secs(5)).await; | 
				
			||||
        } | 
				
			||||
    }; | 
				
			||||
} | 
				
			||||
const ITERATION_LATENCY: u64 = 5; | 
				
			||||
 | 
				
			||||
pub async fn process_request(job_requests: Vec<AssignedJob>, client: &ClientHandler) { | 
				
			||||
    if job_requests.len() > 0 { | 
				
			||||
        for jr in &job_requests { | 
				
			||||
            if !JobCache::contains(&jr.job_id) { | 
				
			||||
pub async fn process_request(jobs: Vec<AssignedJobById>, client: &ClientHandler) { | 
				
			||||
    if !jobs.is_empty() { | 
				
			||||
        for jr in &jobs { | 
				
			||||
            if !JobCache::contains(jr.job_id) { | 
				
			||||
                info!("Fetching job: {}", &jr.job_id); | 
				
			||||
                let fetched_job = retry_until_ok!(client.get_jobs(Some(jr.job_id)).await) | 
				
			||||
                    .pop() | 
				
			||||
                    .unwrap(); | 
				
			||||
                let fetched_job = loop { | 
				
			||||
                    match client.get_jobs(Some(jr.job_id)).await { | 
				
			||||
                        Ok(mut result) => break result.pop().unwrap(), | 
				
			||||
                        Err(err) => { | 
				
			||||
                            debug!("{:?} \nretrying...", err); | 
				
			||||
                            sleep(Duration::from_secs(ITERATION_LATENCY)).await; | 
				
			||||
                        } | 
				
			||||
                    } | 
				
			||||
                }; | 
				
			||||
                JobCache::insert(fetched_job); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        info!( | 
				
			||||
            "Scheduling jobs: {}", | 
				
			||||
            job_requests | 
				
			||||
                .iter() | 
				
			||||
            jobs.iter() | 
				
			||||
                .map(|j| j.job_id.to_string()) | 
				
			||||
                .collect::<Vec<String>>() | 
				
			||||
                .join(", ") | 
				
			||||
        ); | 
				
			||||
        let mut builder = JobBuilder::from_request(job_requests); | 
				
			||||
        let errors = builder.pop_errors(); | 
				
			||||
        if errors.len() > 0 { | 
				
			||||
            error!( | 
				
			||||
                "Some errors encountered: \n{}", | 
				
			||||
                errors | 
				
			||||
                    .iter() | 
				
			||||
                    .map(|j| j.to_string()) | 
				
			||||
                    .collect::<Vec<String>>() | 
				
			||||
                    .join("\n") | 
				
			||||
            ); | 
				
			||||
        let mut runner = JobRunner::from_jobs(jobs); | 
				
			||||
        let errors = runner.pop_errors(); | 
				
			||||
        if !errors.is_empty() { | 
				
			||||
            for e in errors { | 
				
			||||
                ErrChan::send(e, "ebld").await; | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        builder.unwrap_one().spawn().await; | 
				
			||||
        runner.unwrap_one().spawn().await; | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub async fn run_forever() { | 
				
			||||
    //daemonize();
 | 
				
			||||
    env_logger::init(); | 
				
			||||
    let arg_ip = env::args().nth(1); | 
				
			||||
    let instance = ClientHandler::new(arg_ip.as_deref()); | 
				
			||||
    info!("Connecting to the server"); | 
				
			||||
async fn error_reporting(client: Arc<ClientHandler>) -> ! { | 
				
			||||
    loop { | 
				
			||||
        let job_requests: Vec<AssignedJob> = | 
				
			||||
            retry_until_ok!(instance.get_personal_jobs(Some(*UID)).await).into_builtin_vec(); | 
				
			||||
        process_request(job_requests, &instance).await; | 
				
			||||
        let result: Vec<ExecResult> = pop_completed().await.into_iter().collect(); | 
				
			||||
        if result.len() > 0 { | 
				
			||||
            retry_until_ok!(instance.report(&result).await); | 
				
			||||
        match ErrChan::recv().await { | 
				
			||||
            Some(err) => { | 
				
			||||
                'retry: for _ in 0..3 { | 
				
			||||
                    match client.report(Reportable::Error(err.clone())).await { | 
				
			||||
                        Ok(_) => break 'retry, | 
				
			||||
                        Err(e) => { | 
				
			||||
                            debug!("Reporting error: {:?}", e); | 
				
			||||
                            sleep(Duration::from_secs(10)).await; | 
				
			||||
                        } | 
				
			||||
                    } | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        sleep(Duration::from_secs(5)).await; | 
				
			||||
            None => sleep(Duration::from_secs(3)).await, | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
async fn agent_loop(client: Arc<ClientHandler>) -> ! { | 
				
			||||
    loop { | 
				
			||||
        match client.get_personal_jobs(get_self_uid()).await { | 
				
			||||
            Ok(jobs) => { | 
				
			||||
                process_request(jobs, &client).await; | 
				
			||||
            } | 
				
			||||
            Err(err) => ErrChan::send(err, "processing").await, | 
				
			||||
        } | 
				
			||||
        let result: Vec<Reportable> = pop_completed() | 
				
			||||
            .await | 
				
			||||
            .into_iter() | 
				
			||||
            .map(|result| match result { | 
				
			||||
                Ok(r) => Reportable::Assigned(r), | 
				
			||||
                Err(e) => Reportable::Error(e), | 
				
			||||
            }) | 
				
			||||
            .collect(); | 
				
			||||
        if !result.is_empty() { | 
				
			||||
            if let Err(err) = client.report(result).await { | 
				
			||||
                ErrChan::send(err, "report").await; | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        sleep(Duration::from_secs(ITERATION_LATENCY)).await; | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub async fn run_forever() -> ! { | 
				
			||||
    let env = load_env_default().unwrap(); | 
				
			||||
    let client = Arc::new(ClientHandler::new(&env.u_server, None)); | 
				
			||||
    tokio::spawn(error_reporting(client.clone())); | 
				
			||||
 | 
				
			||||
    if cfg!(debug_assertions) { | 
				
			||||
        init_logger(Some(format!( | 
				
			||||
            "u_agent-{}", | 
				
			||||
            get_self_uid() | 
				
			||||
                .hyphenated() | 
				
			||||
                .to_string() | 
				
			||||
                .split("-") | 
				
			||||
                .next() | 
				
			||||
                .unwrap() | 
				
			||||
        ))); | 
				
			||||
        // } else {
 | 
				
			||||
        //     if let Err(e) = Daemonize::new().start() {
 | 
				
			||||
        //         ErrChan::send(UError::Runtime(e.to_string()), "deeeemon").await
 | 
				
			||||
        //     }
 | 
				
			||||
    } | 
				
			||||
    info!("Starting agent {}", get_self_uid()); | 
				
			||||
    agent_loop(client).await | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -0,0 +1,129 @@ | 
				
			||||
use serde_json::{from_str, to_value, Value}; | 
				
			||||
use structopt::StructOpt; | 
				
			||||
use u_lib::{ | 
				
			||||
    api::ClientHandler, | 
				
			||||
    datatypes::PanelResult, | 
				
			||||
    messaging::AsMsg, | 
				
			||||
    models::{Agent, AssignedJob, JobMeta}, | 
				
			||||
    UError, UResult, | 
				
			||||
}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
pub struct Args { | 
				
			||||
    #[structopt(subcommand)] | 
				
			||||
    cmd: Cmd, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
enum Cmd { | 
				
			||||
    Agents(RUD), | 
				
			||||
    Jobs(JobCRUD), | 
				
			||||
    Map(JobMapCRUD), | 
				
			||||
    Ping, | 
				
			||||
    Serve, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
enum JobCRUD { | 
				
			||||
    Create { | 
				
			||||
        job: String, | 
				
			||||
    }, | 
				
			||||
    #[structopt(flatten)] | 
				
			||||
    RUD(RUD), | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
enum JobCmd { | 
				
			||||
    #[structopt(external_subcommand)] | 
				
			||||
    Cmd(Vec<String>), | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
enum JobMapCRUD { | 
				
			||||
    Create { | 
				
			||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
				
			||||
        agent_uid: Uuid, | 
				
			||||
 | 
				
			||||
        job_idents: Vec<String>, | 
				
			||||
    }, | 
				
			||||
    #[structopt(flatten)] | 
				
			||||
    RUD(RUD), | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
enum RUD { | 
				
			||||
    Read { | 
				
			||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
				
			||||
        uid: Option<Uuid>, | 
				
			||||
    }, | 
				
			||||
    Update { | 
				
			||||
        item: String, | 
				
			||||
    }, | 
				
			||||
    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 fn into_value<M: AsMsg>(data: M) -> Value { | 
				
			||||
    to_value(data).unwrap() | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub async fn process_cmd(client: ClientHandler, args: Args) -> PanelResult<Value> { | 
				
			||||
    let catcher: UResult<Value> = (|| async { | 
				
			||||
        Ok(match args.cmd { | 
				
			||||
            Cmd::Agents(action) => match action { | 
				
			||||
                RUD::Read { uid } => into_value(client.get_agents(uid).await?), | 
				
			||||
                RUD::Update { item } => { | 
				
			||||
                    let agent = from_str::<Agent>(&item)?; | 
				
			||||
                    into_value(client.update_agent(agent).await?) | 
				
			||||
                } | 
				
			||||
                RUD::Delete { uid } => into_value(client.del(uid).await?), | 
				
			||||
            }, | 
				
			||||
            Cmd::Jobs(action) => match action { | 
				
			||||
                JobCRUD::Create { job } => { | 
				
			||||
                    let raw_job = from_str::<JobMeta>(&job)?; | 
				
			||||
                    let job = raw_job.validated()?; | 
				
			||||
                    into_value(client.upload_jobs(job).await?) | 
				
			||||
                } | 
				
			||||
                JobCRUD::RUD(RUD::Read { uid }) => into_value(client.get_jobs(uid).await?), | 
				
			||||
                JobCRUD::RUD(RUD::Update { item }) => { | 
				
			||||
                    let raw_job = from_str::<JobMeta>(&item)?; | 
				
			||||
                    let job = raw_job.validated()?; | 
				
			||||
                    into_value(client.update_job(job).await?) | 
				
			||||
                } | 
				
			||||
                JobCRUD::RUD(RUD::Delete { uid }) => into_value(client.del(uid).await?), | 
				
			||||
            }, | 
				
			||||
            Cmd::Map(action) => match action { | 
				
			||||
                JobMapCRUD::Create { | 
				
			||||
                    agent_uid, | 
				
			||||
                    job_idents, | 
				
			||||
                } => into_value(client.set_jobs(agent_uid, job_idents).await?), | 
				
			||||
                JobMapCRUD::RUD(RUD::Read { uid }) => into_value(client.get_agent_jobs(uid).await?), | 
				
			||||
                JobMapCRUD::RUD(RUD::Update { item }) => { | 
				
			||||
                    let assigned = from_str::<AssignedJob>(&item)?; | 
				
			||||
                    into_value(client.update_result(assigned).await?) | 
				
			||||
                } | 
				
			||||
                JobMapCRUD::RUD(RUD::Delete { uid }) => into_value(client.del(uid).await?), | 
				
			||||
            }, | 
				
			||||
            Cmd::Ping => into_value(client.ping().await?), | 
				
			||||
            Cmd::Serve => { | 
				
			||||
                crate::server::serve(client) | 
				
			||||
                    .await | 
				
			||||
                    .map_err(|e| UError::PanelError(format!("{e:?}")))?; | 
				
			||||
                Value::Null | 
				
			||||
            } | 
				
			||||
        }) | 
				
			||||
    })() | 
				
			||||
    .await; | 
				
			||||
 | 
				
			||||
    match catcher { | 
				
			||||
        Ok(r) => PanelResult::Ok(r), | 
				
			||||
        Err(e) => PanelResult::Err(e), | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -1,143 +1,33 @@ | 
				
			||||
use std::env; | 
				
			||||
use std::fmt; | 
				
			||||
use structopt::StructOpt; | 
				
			||||
use u_lib::{ | 
				
			||||
    api::ClientHandler, datatypes::DataResult, messaging::AsMsg, models::JobMeta, utils::init_env, | 
				
			||||
    UResult, | 
				
			||||
}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
struct Args { | 
				
			||||
    #[structopt(subcommand)] | 
				
			||||
    cmd: Cmd, | 
				
			||||
    #[structopt(long)] | 
				
			||||
    json: bool, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(StructOpt, Debug)] | 
				
			||||
enum Cmd { | 
				
			||||
    Agents(LD), | 
				
			||||
    Jobs(JobALD), | 
				
			||||
    Jobmap(JobMapALD), | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[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, | 
				
			||||
mod argparse; | 
				
			||||
mod server; | 
				
			||||
 | 
				
			||||
        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, | 
				
			||||
    }, | 
				
			||||
} | 
				
			||||
#[macro_use] | 
				
			||||
extern crate tracing; | 
				
			||||
 | 
				
			||||
fn parse_uuid(src: &str) -> Result<Uuid, String> { | 
				
			||||
    Uuid::parse_str(src).map_err(|e| e.to_string()) | 
				
			||||
} | 
				
			||||
use anyhow::Result as AnyResult; | 
				
			||||
use argparse::{process_cmd, Args}; | 
				
			||||
use serde::Deserialize; | 
				
			||||
use structopt::StructOpt; | 
				
			||||
use u_lib::api::ClientHandler; | 
				
			||||
use u_lib::logging::init_logger; | 
				
			||||
use u_lib::utils::{env::default_host, load_env}; | 
				
			||||
 | 
				
			||||
async fn process_cmd(args: Args) { | 
				
			||||
    struct Printer { | 
				
			||||
        json: bool, | 
				
			||||
#[derive(Deserialize)] | 
				
			||||
struct AccessEnv { | 
				
			||||
    admin_auth_token: String, | 
				
			||||
    #[serde(default = "default_host")] | 
				
			||||
    u_server: String, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
    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), | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
#[actix_web::main] | 
				
			||||
async fn main() -> AnyResult<()> { | 
				
			||||
    let env = load_env::<AccessEnv>()?; | 
				
			||||
    let client = ClientHandler::new(&env.u_server, Some(env.admin_auth_token)); | 
				
			||||
    let args = Args::from_args(); | 
				
			||||
 | 
				
			||||
    let token = env::var("ADMIN_AUTH_TOKEN").expect("Authentication token is not set"); | 
				
			||||
    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, | 
				
			||||
                agent: _agent, | 
				
			||||
            } => { | 
				
			||||
                let job = JobMeta::builder() | 
				
			||||
                    .with_shell(cmd.join(" ")) | 
				
			||||
                    .with_alias(alias) | 
				
			||||
                    .build() | 
				
			||||
                    .unwrap(); | 
				
			||||
                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), | 
				
			||||
        }, | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
    init_logger(None::<&str>); | 
				
			||||
 | 
				
			||||
#[tokio::main] | 
				
			||||
async fn main() { | 
				
			||||
    init_env(); | 
				
			||||
    let args: Args = Args::from_args(); | 
				
			||||
    process_cmd(args).await; | 
				
			||||
    let result = process_cmd(client, args).await.to_string(); | 
				
			||||
    println!("{result}"); | 
				
			||||
    Ok(()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -0,0 +1,17 @@ | 
				
			||||
use actix_web::http::StatusCode; | 
				
			||||
use actix_web::ResponseError; | 
				
			||||
 | 
				
			||||
#[derive(thiserror::Error, Debug)] | 
				
			||||
pub enum Error { | 
				
			||||
    #[error("Arg parse error: {0}")] | 
				
			||||
    ArgparseError(#[from] structopt::clap::Error), | 
				
			||||
 | 
				
			||||
    #[error("Just an error: {0}")] | 
				
			||||
    JustError(String), | 
				
			||||
} | 
				
			||||
 | 
				
			||||
impl ResponseError for Error { | 
				
			||||
    fn status_code(&self) -> actix_web::http::StatusCode { | 
				
			||||
        StatusCode::BAD_REQUEST | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,48 @@ | 
				
			||||
# See http://help.github.com/ignore-files/ for more about ignoring files. | 
				
			||||
 | 
				
			||||
# compiled output | 
				
			||||
/dist | 
				
			||||
/tmp | 
				
			||||
/out-tsc | 
				
			||||
# Only exists if Bazel was run | 
				
			||||
/bazel-out | 
				
			||||
 | 
				
			||||
# dependencies | 
				
			||||
/node_modules | 
				
			||||
 | 
				
			||||
# profiling files | 
				
			||||
chrome-profiler-events*.json | 
				
			||||
 | 
				
			||||
# IDEs and editors | 
				
			||||
/.idea | 
				
			||||
.project | 
				
			||||
.classpath | 
				
			||||
.c9/ | 
				
			||||
*.launch | 
				
			||||
.settings/ | 
				
			||||
*.sublime-workspace | 
				
			||||
 | 
				
			||||
# IDE - VSCode | 
				
			||||
.vscode/* | 
				
			||||
!.vscode/settings.json | 
				
			||||
!.vscode/tasks.json | 
				
			||||
!.vscode/launch.json | 
				
			||||
!.vscode/extensions.json | 
				
			||||
.history/* | 
				
			||||
 | 
				
			||||
# misc | 
				
			||||
/.angular/cache | 
				
			||||
/.sass-cache | 
				
			||||
/connect.lock | 
				
			||||
/coverage | 
				
			||||
/libpeerconnection.log | 
				
			||||
npm-debug.log | 
				
			||||
yarn-error.log | 
				
			||||
testem.log | 
				
			||||
/typings | 
				
			||||
 | 
				
			||||
# System Files | 
				
			||||
.DS_Store | 
				
			||||
Thumbs.db | 
				
			||||
 | 
				
			||||
package-lock.json | 
				
			||||
@ -0,0 +1,27 @@ | 
				
			||||
# Fe | 
				
			||||
 | 
				
			||||
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 13.1.2. | 
				
			||||
 | 
				
			||||
## Development server | 
				
			||||
 | 
				
			||||
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files. | 
				
			||||
 | 
				
			||||
## Code scaffolding | 
				
			||||
 | 
				
			||||
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. | 
				
			||||
 | 
				
			||||
## Build | 
				
			||||
 | 
				
			||||
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. | 
				
			||||
 | 
				
			||||
## Running unit tests | 
				
			||||
 | 
				
			||||
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). | 
				
			||||
 | 
				
			||||
## Running end-to-end tests | 
				
			||||
 | 
				
			||||
Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. | 
				
			||||
 | 
				
			||||
## Further help | 
				
			||||
 | 
				
			||||
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. | 
				
			||||
@ -0,0 +1,114 @@ | 
				
			||||
{ | 
				
			||||
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json", | 
				
			||||
  "version": 1, | 
				
			||||
  "newProjectRoot": "projects", | 
				
			||||
  "projects": { | 
				
			||||
    "fe": { | 
				
			||||
      "projectType": "application", | 
				
			||||
      "schematics": { | 
				
			||||
        "@schematics/angular:component": { | 
				
			||||
          "style": "less" | 
				
			||||
        }, | 
				
			||||
        "@schematics/angular:application": { | 
				
			||||
          "strict": true | 
				
			||||
        } | 
				
			||||
      }, | 
				
			||||
      "root": "", | 
				
			||||
      "sourceRoot": "src", | 
				
			||||
      "prefix": "app", | 
				
			||||
      "architect": { | 
				
			||||
        "build": { | 
				
			||||
          "builder": "@angular-devkit/build-angular:browser", | 
				
			||||
          "options": { | 
				
			||||
            "outputPath": "dist/fe", | 
				
			||||
            "index": "src/index.html", | 
				
			||||
            "main": "src/main.ts", | 
				
			||||
            "polyfills": "src/polyfills.ts", | 
				
			||||
            "tsConfig": "tsconfig.app.json", | 
				
			||||
            "inlineStyleLanguage": "less", | 
				
			||||
            "assets": [ | 
				
			||||
              "src/favicon.ico", | 
				
			||||
              "src/assets" | 
				
			||||
            ], | 
				
			||||
            "styles": [ | 
				
			||||
              "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", | 
				
			||||
              "src/styles.less" | 
				
			||||
            ], | 
				
			||||
            "scripts": [] | 
				
			||||
          }, | 
				
			||||
          "configurations": { | 
				
			||||
            "production": { | 
				
			||||
              "baseHref": "/core/", | 
				
			||||
              "budgets": [ | 
				
			||||
                { | 
				
			||||
                  "type": "initial", | 
				
			||||
                  "maximumWarning": "500kb", | 
				
			||||
                  "maximumError": "1mb" | 
				
			||||
                }, | 
				
			||||
                { | 
				
			||||
                  "type": "anyComponentStyle", | 
				
			||||
                  "maximumWarning": "2kb", | 
				
			||||
                  "maximumError": "4kb" | 
				
			||||
                } | 
				
			||||
              ], | 
				
			||||
              "fileReplacements": [ | 
				
			||||
                { | 
				
			||||
                  "replace": "src/environments/environment.ts", | 
				
			||||
                  "with": "src/environments/environment.prod.ts" | 
				
			||||
                } | 
				
			||||
              ], | 
				
			||||
              "outputHashing": "all" | 
				
			||||
            }, | 
				
			||||
            "development": { | 
				
			||||
              "buildOptimizer": false, | 
				
			||||
              "optimization": false, | 
				
			||||
              "vendorChunk": true, | 
				
			||||
              "extractLicenses": false, | 
				
			||||
              "sourceMap": true, | 
				
			||||
              "namedChunks": true | 
				
			||||
            } | 
				
			||||
          }, | 
				
			||||
          "defaultConfiguration": "production" | 
				
			||||
        }, | 
				
			||||
        "serve": { | 
				
			||||
          "builder": "@angular-devkit/build-angular:dev-server", | 
				
			||||
          "configurations": { | 
				
			||||
            "production": { | 
				
			||||
              "browserTarget": "fe:build:production" | 
				
			||||
            }, | 
				
			||||
            "development": { | 
				
			||||
              "browserTarget": "fe:build:development" | 
				
			||||
            } | 
				
			||||
          }, | 
				
			||||
          "defaultConfiguration": "development" | 
				
			||||
        }, | 
				
			||||
        "extract-i18n": { | 
				
			||||
          "builder": "@angular-devkit/build-angular:extract-i18n", | 
				
			||||
          "options": { | 
				
			||||
            "browserTarget": "fe:build" | 
				
			||||
          } | 
				
			||||
        }, | 
				
			||||
        "test": { | 
				
			||||
          "builder": "@angular-devkit/build-angular:karma", | 
				
			||||
          "options": { | 
				
			||||
            "main": "src/test.ts", | 
				
			||||
            "polyfills": "src/polyfills.ts", | 
				
			||||
            "tsConfig": "tsconfig.spec.json", | 
				
			||||
            "karmaConfig": "karma.conf.js", | 
				
			||||
            "inlineStyleLanguage": "less", | 
				
			||||
            "assets": [ | 
				
			||||
              "src/favicon.ico", | 
				
			||||
              "src/assets" | 
				
			||||
            ], | 
				
			||||
            "styles": [ | 
				
			||||
              "./node_modules/@angular/material/prebuilt-themes/deeppurple-amber.css", | 
				
			||||
              "src/styles.less" | 
				
			||||
            ], | 
				
			||||
            "scripts": [] | 
				
			||||
          } | 
				
			||||
        } | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
  }, | 
				
			||||
  "defaultProject": "fe" | 
				
			||||
} | 
				
			||||
@ -0,0 +1,44 @@ | 
				
			||||
// Karma configuration file, see link for more information
 | 
				
			||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
 | 
				
			||||
 | 
				
			||||
module.exports = function (config) { | 
				
			||||
  config.set({ | 
				
			||||
    basePath: '', | 
				
			||||
    frameworks: ['jasmine', '@angular-devkit/build-angular'], | 
				
			||||
    plugins: [ | 
				
			||||
      require('karma-jasmine'), | 
				
			||||
      require('karma-chrome-launcher'), | 
				
			||||
      require('karma-jasmine-html-reporter'), | 
				
			||||
      require('karma-coverage'), | 
				
			||||
      require('@angular-devkit/build-angular/plugins/karma') | 
				
			||||
    ], | 
				
			||||
    client: { | 
				
			||||
      jasmine: { | 
				
			||||
        // you can add configuration options for Jasmine here
 | 
				
			||||
        // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html
 | 
				
			||||
        // for example, you can disable the random execution with `random: false`
 | 
				
			||||
        // or set a specific seed with `seed: 4321`
 | 
				
			||||
      }, | 
				
			||||
      clearContext: false // leave Jasmine Spec Runner output visible in browser
 | 
				
			||||
    }, | 
				
			||||
    jasmineHtmlReporter: { | 
				
			||||
      suppressAll: true // removes the duplicated traces
 | 
				
			||||
    }, | 
				
			||||
    coverageReporter: { | 
				
			||||
      dir: require('path').join(__dirname, './coverage/fe'), | 
				
			||||
      subdir: '.', | 
				
			||||
      reporters: [ | 
				
			||||
        { type: 'html' }, | 
				
			||||
        { type: 'text-summary' } | 
				
			||||
      ] | 
				
			||||
    }, | 
				
			||||
    reporters: ['progress', 'kjhtml'], | 
				
			||||
    port: 9876, | 
				
			||||
    colors: true, | 
				
			||||
    logLevel: config.LOG_INFO, | 
				
			||||
    autoWatch: true, | 
				
			||||
    browsers: ['Chrome'], | 
				
			||||
    singleRun: false, | 
				
			||||
    restartOnFileChange: true | 
				
			||||
  }); | 
				
			||||
}; | 
				
			||||
@ -0,0 +1,43 @@ | 
				
			||||
{ | 
				
			||||
  "name": "fe", | 
				
			||||
  "version": "0.0.0", | 
				
			||||
  "scripts": { | 
				
			||||
    "ng": "ng", | 
				
			||||
    "start": "ng serve", | 
				
			||||
    "build": "ng build", | 
				
			||||
    "watch": "ng build --watch --configuration development", | 
				
			||||
    "test": "ng test" | 
				
			||||
  }, | 
				
			||||
  "private": true, | 
				
			||||
  "dependencies": { | 
				
			||||
    "@angular/animations": "~13.1.0", | 
				
			||||
    "@angular/cdk": "^13.3.9", | 
				
			||||
    "@angular/common": "~13.1.0", | 
				
			||||
    "@angular/compiler": "~13.1.0", | 
				
			||||
    "@angular/core": "~13.1.0", | 
				
			||||
    "@angular/forms": "~13.1.0", | 
				
			||||
    "@angular/material": "^13.3.9", | 
				
			||||
    "@angular/platform-browser": "~13.1.0", | 
				
			||||
    "@angular/platform-browser-dynamic": "~13.1.0", | 
				
			||||
    "@angular/router": "~13.1.0", | 
				
			||||
    "@types/uuid": "^8.3.4", | 
				
			||||
    "rxjs": "~7.4.0", | 
				
			||||
    "tslib": "^2.3.0", | 
				
			||||
    "uuid": "^8.3.2", | 
				
			||||
    "zone.js": "~0.11.4" | 
				
			||||
  }, | 
				
			||||
  "devDependencies": { | 
				
			||||
    "@angular-devkit/build-angular": "^13.3.9", | 
				
			||||
    "@angular/cli": "~13.1.2", | 
				
			||||
    "@angular/compiler-cli": "~13.1.0", | 
				
			||||
    "@types/jasmine": "~3.10.0", | 
				
			||||
    "@types/node": "^12.11.1", | 
				
			||||
    "jasmine-core": "~3.10.0", | 
				
			||||
    "karma": "~6.3.0", | 
				
			||||
    "karma-chrome-launcher": "~3.1.0", | 
				
			||||
    "karma-coverage": "~2.1.0", | 
				
			||||
    "karma-jasmine": "~4.0.0", | 
				
			||||
    "karma-jasmine-html-reporter": "~1.7.0", | 
				
			||||
    "typescript": "~4.5.2" | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,19 @@ | 
				
			||||
import { NgModule } from '@angular/core'; | 
				
			||||
import { RouterModule, Routes } from '@angular/router'; | 
				
			||||
import { AgentComponent } from './core/tables/agent.component'; | 
				
			||||
import { JobComponent } from './core/tables/job.component'; | 
				
			||||
import { ResultComponent } from './core/tables/result.component'; | 
				
			||||
import { AgentInfoDialogComponent } from './core/tables/dialogs/agent_info.component'; | 
				
			||||
 | 
				
			||||
const routes: Routes = [ | 
				
			||||
  { path: '', redirectTo: 'agents', pathMatch: 'full' }, | 
				
			||||
  { path: 'agents', component: AgentComponent }, | 
				
			||||
  { path: 'jobs', component: JobComponent }, | 
				
			||||
  { path: 'results', component: ResultComponent }, | 
				
			||||
]; | 
				
			||||
 | 
				
			||||
@NgModule({ | 
				
			||||
  imports: [RouterModule.forRoot(routes)], | 
				
			||||
  exports: [RouterModule] | 
				
			||||
}) | 
				
			||||
export class AppRoutingModule { } | 
				
			||||
@ -0,0 +1,5 @@ | 
				
			||||
<nav mat-tab-nav-bar animationDuration="0ms" mat-align-tabs="center"> | 
				
			||||
  <a mat-tab-link *ngFor="let tab of tabs" routerLink={{tab.link}} routerLinkActive #rla="routerLinkActive" | 
				
			||||
    [active]="rla.isActive" [routerLinkActiveOptions]="{ exact: true }">{{tab.name}}</a> | 
				
			||||
</nav> | 
				
			||||
<router-outlet></router-outlet> | 
				
			||||
@ -0,0 +1,35 @@ | 
				
			||||
import { TestBed } from '@angular/core/testing'; | 
				
			||||
import { RouterTestingModule } from '@angular/router/testing'; | 
				
			||||
import { AppComponent } from './app.component'; | 
				
			||||
 | 
				
			||||
describe('AppComponent', () => { | 
				
			||||
  beforeEach(async () => { | 
				
			||||
    await TestBed.configureTestingModule({ | 
				
			||||
      imports: [ | 
				
			||||
        RouterTestingModule | 
				
			||||
      ], | 
				
			||||
      declarations: [ | 
				
			||||
        AppComponent | 
				
			||||
      ], | 
				
			||||
    }).compileComponents(); | 
				
			||||
  }); | 
				
			||||
 | 
				
			||||
  it('should create the app', () => { | 
				
			||||
    const fixture = TestBed.createComponent(AppComponent); | 
				
			||||
    const app = fixture.componentInstance; | 
				
			||||
    expect(app).toBeTruthy(); | 
				
			||||
  }); | 
				
			||||
 | 
				
			||||
  it(`should have as title 'fe'`, () => { | 
				
			||||
    const fixture = TestBed.createComponent(AppComponent); | 
				
			||||
    const app = fixture.componentInstance; | 
				
			||||
    expect(app.title).toEqual('fe'); | 
				
			||||
  }); | 
				
			||||
 | 
				
			||||
  it('should render title', () => { | 
				
			||||
    const fixture = TestBed.createComponent(AppComponent); | 
				
			||||
    fixture.detectChanges(); | 
				
			||||
    const compiled = fixture.nativeElement as HTMLElement; | 
				
			||||
    expect(compiled.querySelector('.content span')?.textContent).toContain('fe app is running!'); | 
				
			||||
  }); | 
				
			||||
}); | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
import { Component, ViewChild, AfterViewInit } from '@angular/core'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
  selector: 'app-root', | 
				
			||||
  templateUrl: './app.component.html', | 
				
			||||
  styleUrls: ['./app.component.less'] | 
				
			||||
}) | 
				
			||||
export class AppComponent { | 
				
			||||
  tabs = [ | 
				
			||||
    { name: 'Agents', link: '/agents' }, | 
				
			||||
    { name: 'Jobs', link: '/jobs' }, | 
				
			||||
    { name: 'Results', link: '/results' } | 
				
			||||
  ]; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,60 @@ | 
				
			||||
import { NgModule } from '@angular/core'; | 
				
			||||
import { BrowserModule } from '@angular/platform-browser'; | 
				
			||||
import { AppRoutingModule } from './app-routing.module'; | 
				
			||||
import { AppComponent } from './app.component'; | 
				
			||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; | 
				
			||||
import { MatTabsModule } from '@angular/material/tabs'; | 
				
			||||
import { MatTableModule } from '@angular/material/table'; | 
				
			||||
import { MatFormFieldModule } from '@angular/material/form-field'; | 
				
			||||
import { MatButtonModule } from '@angular/material/button' | 
				
			||||
import { MatInputModule } from '@angular/material/input'; | 
				
			||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; | 
				
			||||
import { HttpClientModule } from '@angular/common/http'; | 
				
			||||
import { MatDialogModule } from '@angular/material/dialog'; | 
				
			||||
import { MatIconModule } from '@angular/material/icon'; | 
				
			||||
import { FormsModule } from '@angular/forms'; | 
				
			||||
import { AgentComponent, JobComponent, ResultComponent } from './core/tables'; | 
				
			||||
import { | 
				
			||||
  AgentInfoDialogComponent, | 
				
			||||
  AssignJobDialogComponent, | 
				
			||||
  JobInfoDialogComponent, | 
				
			||||
  ResultInfoDialogComponent | 
				
			||||
} from './core/tables/dialogs'; | 
				
			||||
import { APP_BASE_HREF } from '@angular/common'; | 
				
			||||
import { MatTooltipModule } from '@angular/material/tooltip'; | 
				
			||||
import { MatSnackBarModule } from '@angular/material/snack-bar'; | 
				
			||||
import { MatListModule } from '@angular/material/list'; | 
				
			||||
 | 
				
			||||
@NgModule({ | 
				
			||||
  declarations: [ | 
				
			||||
    AppComponent, | 
				
			||||
    AgentComponent, | 
				
			||||
    JobComponent, | 
				
			||||
    ResultComponent, | 
				
			||||
    AgentInfoDialogComponent, | 
				
			||||
    JobInfoDialogComponent, | 
				
			||||
    ResultInfoDialogComponent, | 
				
			||||
    AssignJobDialogComponent | 
				
			||||
  ], | 
				
			||||
  imports: [ | 
				
			||||
    BrowserModule, | 
				
			||||
    HttpClientModule, | 
				
			||||
    AppRoutingModule, | 
				
			||||
    MatTabsModule, | 
				
			||||
    MatTableModule, | 
				
			||||
    MatButtonModule, | 
				
			||||
    MatFormFieldModule, | 
				
			||||
    MatInputModule, | 
				
			||||
    MatDialogModule, | 
				
			||||
    MatProgressSpinnerModule, | 
				
			||||
    MatIconModule, | 
				
			||||
    MatTooltipModule, | 
				
			||||
    MatSnackBarModule, | 
				
			||||
    MatListModule, | 
				
			||||
    FormsModule, | 
				
			||||
    BrowserAnimationsModule | 
				
			||||
  ], | 
				
			||||
  providers: [{ provide: APP_BASE_HREF, useValue: '/' }], | 
				
			||||
  bootstrap: [AppComponent] | 
				
			||||
}) | 
				
			||||
export class AppModule { } | 
				
			||||
@ -0,0 +1 @@ | 
				
			||||
export * from './services'; 
 | 
				
			||||
@ -0,0 +1,16 @@ | 
				
			||||
import { UTCDate, ApiModel } from "."; | 
				
			||||
 | 
				
			||||
export interface AgentModel extends ApiModel { | 
				
			||||
    alias: string | null, | 
				
			||||
    hostname: string, | 
				
			||||
    host_info: string, | 
				
			||||
    id: string, | 
				
			||||
    is_root: boolean, | 
				
			||||
    is_root_allowed: boolean, | 
				
			||||
    last_active: UTCDate, | 
				
			||||
    platform: string, | 
				
			||||
    regtime: UTCDate, | 
				
			||||
    state: "new" | "active" | "banned", | 
				
			||||
    token: string | null, | 
				
			||||
    username: string, | 
				
			||||
} | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
export * from './agent.model'; | 
				
			||||
export * from './result.model'; | 
				
			||||
export * from './job.model'; | 
				
			||||
 | 
				
			||||
export interface UTCDate { | 
				
			||||
    secs_since_epoch: number, | 
				
			||||
    nanos_since_epoch: number | 
				
			||||
} | 
				
			||||
 | 
				
			||||
export abstract class ApiModel { } | 
				
			||||
 | 
				
			||||
export interface Empty extends ApiModel { } | 
				
			||||
 | 
				
			||||
export type Area = "agents" | "jobs" | "map"; | 
				
			||||
@ -0,0 +1,12 @@ | 
				
			||||
import { ApiModel } from "."; | 
				
			||||
 | 
				
			||||
export interface JobModel extends ApiModel { | 
				
			||||
    alias: string, | 
				
			||||
    argv: string, | 
				
			||||
    id: string, | 
				
			||||
    exec_type: string, | 
				
			||||
    platform: string, | 
				
			||||
    payload: number[] | null, | 
				
			||||
    payload_path: string | null, | 
				
			||||
    schedule: string | null, | 
				
			||||
} | 
				
			||||
@ -0,0 +1,13 @@ | 
				
			||||
import { UTCDate, ApiModel } from "."; | 
				
			||||
 | 
				
			||||
export interface ResultModel extends ApiModel { | 
				
			||||
    agent_id: string, | 
				
			||||
    alias: string, | 
				
			||||
    created: UTCDate, | 
				
			||||
    id: string, | 
				
			||||
    job_id: string, | 
				
			||||
    result: number[], | 
				
			||||
    state: "Queued" | "Running" | "Finished", | 
				
			||||
    retcode: number | null, | 
				
			||||
    updated: UTCDate, | 
				
			||||
} | 
				
			||||
@ -0,0 +1,53 @@ | 
				
			||||
import { environment } from 'src/environments/environment'; | 
				
			||||
import { HttpClient } from '@angular/common/http'; | 
				
			||||
import { firstValueFrom } from 'rxjs'; | 
				
			||||
import { ApiModel, Empty, Area } from '../models'; | 
				
			||||
 | 
				
			||||
interface ServerResponse<T extends ApiModel> { | 
				
			||||
  status: "ok" | "err", | 
				
			||||
  data: T | string | 
				
			||||
} | 
				
			||||
 | 
				
			||||
export class ApiTableService<T extends ApiModel> { | 
				
			||||
  area: Area; | 
				
			||||
 | 
				
			||||
  constructor(private http: HttpClient, area: Area) { | 
				
			||||
    this.area = area; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  requestUrl = `${environment.server}/cmd/`; | 
				
			||||
 | 
				
			||||
  async req<R extends ApiModel>(cmd: string): Promise<ServerResponse<R>> { | 
				
			||||
    return await firstValueFrom(this.http.post<ServerResponse<R>>(this.requestUrl, cmd)) | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  async getOne(id: string, area: string = this.area): Promise<ServerResponse<T>> { | 
				
			||||
    const resp = await this.req<T[]>(`${area} read ${id}`) | 
				
			||||
    if (resp.data.length === 0) { | 
				
			||||
      return { | 
				
			||||
        status: 'err', | 
				
			||||
        data: `${id} not found in ${area}` | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
    return { | 
				
			||||
      status: resp.status, | 
				
			||||
      data: resp.data[0] | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  async getMany(): Promise<ServerResponse<T[]>> { | 
				
			||||
    return await this.req(`${this.area} read`) | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  async update(item: T): Promise<ServerResponse<Empty>> { | 
				
			||||
    return await this.req(`${this.area} update '${JSON.stringify(item)}'`) | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  async delete(id: string): Promise<ServerResponse<Empty>> { | 
				
			||||
    return await this.req(`${this.area} delete ${id}`) | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  async create(item: string): Promise<ServerResponse<string[]>> { | 
				
			||||
    return await this.req(`${this.area} create ${item}`) | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1 @@ | 
				
			||||
export * from './api.service' | 
				
			||||
@ -0,0 +1,78 @@ | 
				
			||||
<div class="mat-elevation-z8"> | 
				
			||||
 | 
				
			||||
    <div class="table-container"> | 
				
			||||
        <div class="loading-shade" *ngIf="isLoadingResults"> | 
				
			||||
            <mat-spinner *ngIf="isLoadingResults"></mat-spinner> | 
				
			||||
        </div> | 
				
			||||
        <mat-form-field appearance="standard"> | 
				
			||||
            <mat-label>Filter</mat-label> | 
				
			||||
            <input matInput (keyup)="apply_filter($event)" #input> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <button id="refresh_btn" mat-raised-button color="primary" (click)="loadTableData()">Refresh</button> | 
				
			||||
 | 
				
			||||
        <table mat-table fixedLayout="true" [dataSource]="table_data" class="data-table" matSort matSortActive="id" | 
				
			||||
            matSortDisableClear matSortDirection="desc"> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="id"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>ID</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.id}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="alias"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Alias</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.alias}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="username"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>User</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.username}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="hostname"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Hostname</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.hostname}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="last_active"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Last active</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.last_active.secs_since_epoch * 1000 | date:'long'}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="actions"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef></th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    <button mat-icon-button (click)="assignJobs(row.id)"> | 
				
			||||
                        <mat-icon>add_task</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                    | | 
				
			||||
                    <button mat-icon-button routerLink='.' [queryParams]="{id: row.id}"> | 
				
			||||
                        <mat-icon>more_horiz</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                    | | 
				
			||||
                    <button mat-icon-button (click)="deleteItem(row.id)"> | 
				
			||||
                        <mat-icon>delete</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | 
				
			||||
            <tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr> | 
				
			||||
            <tr class="mat-row" *matNoDataRow> | 
				
			||||
                <td class="mat-cell">No data</td> | 
				
			||||
            </tr> | 
				
			||||
        </table> | 
				
			||||
    </div> | 
				
			||||
 | 
				
			||||
    <!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"> | 
				
			||||
    </mat-paginator> --> | 
				
			||||
</div> | 
				
			||||
@ -0,0 +1,52 @@ | 
				
			||||
import { Component, OnDestroy, OnInit } from '@angular/core'; | 
				
			||||
import { TablesComponent } from './table.component'; | 
				
			||||
import { AgentModel } from '../models'; | 
				
			||||
import { AgentInfoDialogComponent } from './dialogs/agent_info.component'; | 
				
			||||
import { HttpErrorResponse } from '@angular/common/http'; | 
				
			||||
import { AssignJobDialogComponent } from './dialogs'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
  selector: 'agent-table', | 
				
			||||
  templateUrl: './agent.component.html', | 
				
			||||
  styleUrls: ['./table.component.less'] | 
				
			||||
}) | 
				
			||||
export class AgentComponent extends TablesComponent<AgentModel> implements OnInit { | 
				
			||||
 | 
				
			||||
  //dialogSubscr!: Subscription;
 | 
				
			||||
  area = 'agents' as const; | 
				
			||||
 | 
				
			||||
  displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions'] | 
				
			||||
 | 
				
			||||
  show_item_dialog(id: string) { | 
				
			||||
    this.data_source!.getOne(id).then(resp => { | 
				
			||||
      if (resp.status === 'ok') { | 
				
			||||
        const dialog = this.infoDialog.open(AgentInfoDialogComponent, { | 
				
			||||
          data: resp.data as AgentModel, | 
				
			||||
          width: '1000px', | 
				
			||||
        }); | 
				
			||||
 | 
				
			||||
        const saveSub = dialog.componentInstance.onSave.subscribe(result => { | 
				
			||||
          this.data_source!.update(result).then(_ => { | 
				
			||||
            this.openSnackBar('Saved', false) | 
				
			||||
            this.loadTableData() | 
				
			||||
          }) | 
				
			||||
            .catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) | 
				
			||||
        }) | 
				
			||||
 | 
				
			||||
        dialog.afterClosed().subscribe(result => { | 
				
			||||
          saveSub.unsubscribe() | 
				
			||||
          this.router.navigate(['.'], { relativeTo: this.route }) | 
				
			||||
        }) | 
				
			||||
      } else { | 
				
			||||
        this.openSnackBar(resp.data) | 
				
			||||
      } | 
				
			||||
    }).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  assignJobs(id: string) { | 
				
			||||
    const dialog = this.infoDialog.open(AssignJobDialogComponent, { | 
				
			||||
      data: id, | 
				
			||||
      width: '1000px', | 
				
			||||
    }); | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,64 @@ | 
				
			||||
<h2 mat-dialog-title *ngIf="is_preview">Agent info</h2> | 
				
			||||
<h2 mat-dialog-title *ngIf="!is_preview">Editing agent info</h2> | 
				
			||||
<mat-dialog-content> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field" cdkFocusInitial> | 
				
			||||
            <mat-label>ID</mat-label> | 
				
			||||
            <input matInput disabled value="{{data.id}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Alias</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.alias"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Username</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.username"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Hostname</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.hostname"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Host info</mat-label> | 
				
			||||
            <textarea matInput cdkTextareaAutosize [readonly]="is_preview" [(ngModel)]="data.host_info"> | 
				
			||||
            </textarea> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Platform</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.platform"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Is root</mat-label> | 
				
			||||
            <input matInput disabled value="{{data.is_root}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Registration time</mat-label> | 
				
			||||
            <input matInput disabled value="{{data.regtime.secs_since_epoch * 1000 | date:'long'}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
    <p> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Last active time</mat-label> | 
				
			||||
            <input matInput disabled value="{{data.last_active.secs_since_epoch * 1000 | date:'long'}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </p> | 
				
			||||
</mat-dialog-content> | 
				
			||||
<mat-dialog-actions align="end"> | 
				
			||||
    <button mat-raised-button *ngIf="is_preview" (click)="is_preview = false">Edit</button> | 
				
			||||
    <button mat-raised-button *ngIf="!is_preview" (click)="updateAgent()">Save</button> | 
				
			||||
    <button mat-button mat-dialog-close>Cancel</button> | 
				
			||||
</mat-dialog-actions> | 
				
			||||
@ -0,0 +1,21 @@ | 
				
			||||
import { Component, Inject } from '@angular/core'; | 
				
			||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; | 
				
			||||
import { AgentModel } from '../../models/agent.model'; | 
				
			||||
import { EventEmitter } from '@angular/core'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
    selector: 'agent-info-dialog', | 
				
			||||
    templateUrl: 'agent-info-dialog.html', | 
				
			||||
    styleUrls: ['info-dialog.component.less'] | 
				
			||||
}) | 
				
			||||
export class AgentInfoDialogComponent { | 
				
			||||
    is_preview = true; | 
				
			||||
    onSave = new EventEmitter(); | 
				
			||||
 | 
				
			||||
    constructor(@Inject(MAT_DIALOG_DATA) public data: AgentModel) { } | 
				
			||||
 | 
				
			||||
    updateAgent() { | 
				
			||||
        console.log(this.data); | 
				
			||||
        this.onSave.emit(this.data); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,12 @@ | 
				
			||||
<h2 mat-dialog-title>Assign job</h2> | 
				
			||||
<mat-dialog-content> | 
				
			||||
    <mat-selection-list #jobsList [(ngModel)]="selected_rows"> | 
				
			||||
        <mat-list-option *ngFor="let row of rows" [value]="row"> | 
				
			||||
            {{row}} | 
				
			||||
        </mat-list-option> | 
				
			||||
    </mat-selection-list> | 
				
			||||
</mat-dialog-content> | 
				
			||||
<mat-dialog-actions align="end"> | 
				
			||||
    <button mat-raised-button mat-dialog-close (click)="assignSelectedJobs()">Assign</button> | 
				
			||||
    <button mat-button mat-dialog-close>Cancel</button> | 
				
			||||
</mat-dialog-actions> | 
				
			||||
@ -0,0 +1,33 @@ | 
				
			||||
import { Component, Inject } from '@angular/core'; | 
				
			||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; | 
				
			||||
import { HttpClient } from '@angular/common/http'; | 
				
			||||
import { ApiTableService } from '../../services'; | 
				
			||||
import { JobModel } from '../../models'; | 
				
			||||
import { MatListOption } from '@angular/material/list'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
    selector: 'assign-job-dialog', | 
				
			||||
    templateUrl: 'assign-job-dialog.html', | 
				
			||||
    styleUrls: [] | 
				
			||||
}) | 
				
			||||
export class AssignJobDialogComponent { | 
				
			||||
    rows: string[] = []; | 
				
			||||
    selected_rows: string[] = []; | 
				
			||||
 | 
				
			||||
    constructor(@Inject(MAT_DIALOG_DATA) public agent_id: string, private http: HttpClient) { | 
				
			||||
        new ApiTableService(http, "jobs").getMany().then(result => { | 
				
			||||
            if (result.status == "ok") { | 
				
			||||
                const jobs = result.data as JobModel[] | 
				
			||||
                this.rows = jobs.map(j => `${j.id} ${j.alias}`) | 
				
			||||
            } else { | 
				
			||||
                alert(result.data as string) | 
				
			||||
            } | 
				
			||||
        }).catch(err => alert(err)) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    assignSelectedJobs() { | 
				
			||||
        const job_ids = this.selected_rows.map(row => row.split(' ', 1)[0]).join(' '); | 
				
			||||
        const request = `${this.agent_id} ${job_ids}` | 
				
			||||
        new ApiTableService(this.http, "map").create(request).catch(err => alert(err)) | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,4 @@ | 
				
			||||
export * from './agent_info.component'; | 
				
			||||
export * from './result_info.component'; | 
				
			||||
export * from './job_info.component'; | 
				
			||||
export * from './assign_job.component'; | 
				
			||||
@ -0,0 +1,14 @@ | 
				
			||||
.info-dlg-field { | 
				
			||||
    width: 100%; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
div.info-dialog-forms-box { | 
				
			||||
    width: 100%; | 
				
			||||
    margin-right: 10px; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
div.info-dialog-forms-box-smol { | 
				
			||||
    width: 30%; | 
				
			||||
    float: left; | 
				
			||||
    margin-right: 10px; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,50 @@ | 
				
			||||
<h2 mat-dialog-title *ngIf="is_preview">Job info</h2> | 
				
			||||
<h2 mat-dialog-title *ngIf="!is_preview">Editing job info</h2> | 
				
			||||
<mat-dialog-content> | 
				
			||||
    <div class="info-dialog-forms-box-smol"> | 
				
			||||
        <mat-form-field class="info-dlg-field" cdkFocusInitial> | 
				
			||||
            <mat-label>ID</mat-label> | 
				
			||||
            <input matInput disabled value="{{data.id}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Alias</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.alias"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Args</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.argv"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
    <div class="info-dialog-forms-box-smol"> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Type</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.exec_type"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Platform</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.platform"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Schedule</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.schedule"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
    <div class="info-dialog-forms-box-smol"> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Payload path</mat-label> | 
				
			||||
            <input matInput [readonly]="is_preview" [(ngModel)]="data.payload_path"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
    <div class="info-dialog-forms-box"> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Payload</mat-label> | 
				
			||||
            <textarea matInput cdkTextareaAutosize [readonly]="is_preview" [(ngModel)]="decodedPayload"> | 
				
			||||
                </textarea> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
</mat-dialog-content> | 
				
			||||
<mat-dialog-actions align="end"> | 
				
			||||
    <button mat-raised-button *ngIf="is_preview" (click)="is_preview = false">Edit</button> | 
				
			||||
    <button mat-raised-button *ngIf="!is_preview" (click)="updateJob()">Save</button> | 
				
			||||
    <button mat-button mat-dialog-close>Cancel</button> | 
				
			||||
</mat-dialog-actions> | 
				
			||||
@ -0,0 +1,30 @@ | 
				
			||||
import { Component, Inject } from '@angular/core'; | 
				
			||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; | 
				
			||||
import { JobModel } from '../../models/job.model'; | 
				
			||||
import { EventEmitter } from '@angular/core'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
    selector: 'job-info-dialog', | 
				
			||||
    templateUrl: 'job-info-dialog.html', | 
				
			||||
    styleUrls: ['info-dialog.component.less'] | 
				
			||||
}) | 
				
			||||
export class JobInfoDialogComponent { | 
				
			||||
    is_preview = true; | 
				
			||||
    decodedPayload: string; | 
				
			||||
    onSave = new EventEmitter(); | 
				
			||||
 | 
				
			||||
    constructor(@Inject(MAT_DIALOG_DATA) public data: JobModel) { | 
				
			||||
        if (data.payload !== null) { | 
				
			||||
            this.decodedPayload = new TextDecoder().decode(new Uint8Array(data.payload)) | 
				
			||||
        } else { | 
				
			||||
            this.decodedPayload = "" | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    updateJob() { | 
				
			||||
        if (this.decodedPayload.length > 0) { | 
				
			||||
            this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload)) | 
				
			||||
        } | 
				
			||||
        this.onSave.emit(this.data); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,53 @@ | 
				
			||||
<h2 mat-dialog-title>Result</h2> | 
				
			||||
<mat-dialog-content> | 
				
			||||
    <div class="info-dialog-forms-box-smol"> | 
				
			||||
        <mat-form-field class="info-dlg-field" cdkFocusInitial> | 
				
			||||
            <mat-label>ID</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.id}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Job ID</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.job_id}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Agent ID</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.agent_id}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
    <div class="info-dialog-forms-box-smol"> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Alias</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.alias}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>State</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.state}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Return code</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.retcode}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
    <div class="info-dialog-forms-box-smol"> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Created</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.created.secs_since_epoch * 1000 | date:'long'}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <mat-form-field class="info-dlg-field"> | 
				
			||||
            <mat-label>Updated</mat-label> | 
				
			||||
            <input matInput readonly value="{{data.updated.secs_since_epoch * 1000 | date:'long'}}"> | 
				
			||||
        </mat-form-field> | 
				
			||||
    </div> | 
				
			||||
    <div class="info-dialog-forms-box"> | 
				
			||||
        <p> | 
				
			||||
            <mat-form-field class="info-dlg-field"> | 
				
			||||
                <mat-label>Result</mat-label> | 
				
			||||
                <textarea matInput cdkTextareaAutosize readonly value="{{decodedResult}}"> | 
				
			||||
                </textarea> | 
				
			||||
            </mat-form-field> | 
				
			||||
        </p> | 
				
			||||
    </div> | 
				
			||||
</mat-dialog-content> | 
				
			||||
<mat-dialog-actions align="end"> | 
				
			||||
    <button mat-button mat-dialog-close>Close</button> | 
				
			||||
</mat-dialog-actions> | 
				
			||||
@ -0,0 +1,20 @@ | 
				
			||||
import { Component, Inject } from '@angular/core'; | 
				
			||||
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; | 
				
			||||
import { ResultModel } from '../../models/result.model'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
    selector: 'result-info-dialog', | 
				
			||||
    templateUrl: 'result-info-dialog.html', | 
				
			||||
    styleUrls: ['info-dialog.component.less'] | 
				
			||||
}) | 
				
			||||
export class ResultInfoDialogComponent { | 
				
			||||
    decodedResult: string; | 
				
			||||
 | 
				
			||||
    constructor(@Inject(MAT_DIALOG_DATA) public data: ResultModel) { | 
				
			||||
        if (data.result !== null) { | 
				
			||||
            this.decodedResult = new TextDecoder().decode(new Uint8Array(data.result)) | 
				
			||||
        } else { | 
				
			||||
            this.decodedResult = "" | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,3 @@ | 
				
			||||
export * from './agent.component'; | 
				
			||||
export * from './job.component'; | 
				
			||||
export * from './result.component'; | 
				
			||||
@ -0,0 +1,83 @@ | 
				
			||||
<div class="mat-elevation-z8"> | 
				
			||||
 | 
				
			||||
    <div class="table-container"> | 
				
			||||
        <div class="loading-shade" *ngIf="isLoadingResults"> | 
				
			||||
            <mat-spinner *ngIf="isLoadingResults"></mat-spinner> | 
				
			||||
        </div> | 
				
			||||
        <mat-form-field appearance="standard"> | 
				
			||||
            <mat-label>Filter</mat-label> | 
				
			||||
            <input matInput (keyup)="apply_filter($event)" #input> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <button id="refresh_btn" mat-raised-button color="basic" (click)="loadTableData()">Refresh</button> | 
				
			||||
        <button id="new_btn" mat-raised-button color="primary" routerLink='.' [queryParams]="{new: true}">Add | 
				
			||||
            job</button> | 
				
			||||
 | 
				
			||||
        <table mat-table fixedLayout="true" [dataSource]="table_data" class="data-table" matSort matSortActive="id" | 
				
			||||
            matSortDisableClear matSortDirection="desc"> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="id"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>ID</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.id}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="alias"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Alias</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.alias}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="argv"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Cmd-line args</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.argv}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="platform"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Platform</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.platform}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="schedule"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Schedule</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.schedule}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="exec_type"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Type</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.exec_type}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="actions"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef></th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    <button mat-icon-button routerLink='.' [queryParams]="{id: row.id}"> | 
				
			||||
                        <mat-icon>more_horiz</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                    | | 
				
			||||
                    <button mat-icon-button (click)="deleteItem(row.id)"> | 
				
			||||
                        <mat-icon>delete</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | 
				
			||||
            <tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr> | 
				
			||||
            <tr class="mat-row" *matNoDataRow> | 
				
			||||
                <td class="mat-cell">No data</td> | 
				
			||||
            </tr> | 
				
			||||
        </table> | 
				
			||||
    </div> | 
				
			||||
 | 
				
			||||
    <!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"> | 
				
			||||
    </mat-paginator> --> | 
				
			||||
</div> | 
				
			||||
@ -0,0 +1,59 @@ | 
				
			||||
import { Component, OnInit } from '@angular/core'; | 
				
			||||
import { TablesComponent } from './table.component'; | 
				
			||||
import { JobModel } from '../models'; | 
				
			||||
import { JobInfoDialogComponent } from './dialogs'; | 
				
			||||
import { HttpErrorResponse } from '@angular/common/http'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
  selector: 'job-table', | 
				
			||||
  templateUrl: './job.component.html', | 
				
			||||
  styleUrls: ['./table.component.less'] | 
				
			||||
}) | 
				
			||||
export class JobComponent extends TablesComponent<JobModel> { | 
				
			||||
  area = 'jobs' as const; | 
				
			||||
  displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] | 
				
			||||
 | 
				
			||||
  show_item_dialog(id: string | null) { | 
				
			||||
    const show_dlg = (id: string, edit: boolean) => { | 
				
			||||
      this.data_source!.getOne(id).then(resp => { | 
				
			||||
        if (resp.status === 'ok') { | 
				
			||||
          var dialog = this.infoDialog.open(JobInfoDialogComponent, { | 
				
			||||
            data: resp.data as JobModel, | 
				
			||||
            width: '1000px', | 
				
			||||
          }); | 
				
			||||
          if (edit) { | 
				
			||||
            dialog.componentInstance.is_preview = false | 
				
			||||
          } | 
				
			||||
 | 
				
			||||
          const saveSub = dialog.componentInstance.onSave.subscribe(result => { | 
				
			||||
            this.data_source!.update(result) | 
				
			||||
              .then(_ => { | 
				
			||||
                this.openSnackBar("Saved", false) | 
				
			||||
                this.loadTableData() | 
				
			||||
              }) | 
				
			||||
              .catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) | 
				
			||||
          }) | 
				
			||||
 | 
				
			||||
          dialog.afterClosed().subscribe(result => { | 
				
			||||
            saveSub.unsubscribe() | 
				
			||||
            this.router.navigate(['.'], { relativeTo: this.route }) | 
				
			||||
          }) | 
				
			||||
        } else { | 
				
			||||
          this.openSnackBar(resp.data) | 
				
			||||
        } | 
				
			||||
      }).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    if (id) { | 
				
			||||
      show_dlg(id, false) | 
				
			||||
    } else { | 
				
			||||
      this.data_source!.create('"{}"').then(resp => { | 
				
			||||
        if (resp.status === 'ok') { | 
				
			||||
          show_dlg(resp.data[0], true) | 
				
			||||
        } else { | 
				
			||||
          this.openSnackBar(resp.data) | 
				
			||||
        } | 
				
			||||
      }).catch((err: HttpErrorResponse) => this.openSnackBar(err.error)) | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,81 @@ | 
				
			||||
<div class="mat-elevation-z8"> | 
				
			||||
 | 
				
			||||
    <div class="table-container"> | 
				
			||||
        <div class="loading-shade" *ngIf="isLoadingResults"> | 
				
			||||
            <mat-spinner *ngIf="isLoadingResults"></mat-spinner> | 
				
			||||
        </div> | 
				
			||||
        <mat-form-field appearance="standard"> | 
				
			||||
            <mat-label>Filter</mat-label> | 
				
			||||
            <input matInput (keyup)="apply_filter($event)" #input> | 
				
			||||
        </mat-form-field> | 
				
			||||
        <button id="refresh_btn" mat-raised-button color="primary" (click)="loadTableData()">Refresh</button> | 
				
			||||
 | 
				
			||||
        <table mat-table fixedLayout="true" [dataSource]="table_data" class="data-table" matSort matSortActive="id" | 
				
			||||
            matSortDisableClear matSortDirection="desc"> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="id"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>ID</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.id}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="alias"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Alias</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.alias}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="agent_id"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Agent</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    <a routerLink='/agents' [queryParams]="{id: row.agent_id}">{{row.agent_id}}</a> | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="job_id"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>Job</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    <a routerLink='/jobs' [queryParams]="{id: row.job_id}">{{row.job_id}}</a> | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="state"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>State</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.state}} {{(row.state === "Finished") ? '(' + row.retcode + ')' : ''}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="last_updated"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef>ID</th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    {{row.updated.secs_since_epoch * 1000| date:'long'}} | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <ng-container matColumnDef="actions"> | 
				
			||||
                <th mat-header-cell *matHeaderCellDef></th> | 
				
			||||
                <td mat-cell *matCellDef="let row"> | 
				
			||||
                    <button mat-icon-button routerLink='.' [queryParams]="{id: row.id}"> | 
				
			||||
                        <mat-icon>more_horiz</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                    | | 
				
			||||
                    <button mat-icon-button (click)="deleteItem(row.id)"> | 
				
			||||
                        <mat-icon>delete</mat-icon> | 
				
			||||
                    </button> | 
				
			||||
                </td> | 
				
			||||
            </ng-container> | 
				
			||||
 | 
				
			||||
            <tr mat-header-row *matHeaderRowDef="displayedColumns"></tr> | 
				
			||||
            <tr mat-row class="data-table-row" *matRowDef="let row; columns: displayedColumns;"></tr> | 
				
			||||
            <tr class="mat-row" *matNoDataRow> | 
				
			||||
                <td class="mat-cell">No data</td> | 
				
			||||
            </tr> | 
				
			||||
        </table> | 
				
			||||
    </div> | 
				
			||||
 | 
				
			||||
    <!-- <mat-paginator [length]="resultsLength" [pageSize]="30" aria-label="Select page of GitHub search results"> | 
				
			||||
    </mat-paginator> --> | 
				
			||||
</div> | 
				
			||||
@ -0,0 +1,41 @@ | 
				
			||||
import { Component, OnInit } from '@angular/core'; | 
				
			||||
import { TablesComponent } from './table.component'; | 
				
			||||
import { ResultModel } from '../models'; | 
				
			||||
import { ResultInfoDialogComponent } from './dialogs'; | 
				
			||||
import { HttpErrorResponse } from '@angular/common/http'; | 
				
			||||
 | 
				
			||||
@Component({ | 
				
			||||
  selector: 'results-table', | 
				
			||||
  templateUrl: './result.component.html', | 
				
			||||
  styleUrls: ['./table.component.less'] | 
				
			||||
}) | 
				
			||||
export class ResultComponent extends TablesComponent<ResultModel> { | 
				
			||||
  area = 'map' as const; | 
				
			||||
 | 
				
			||||
  displayedColumns = [ | 
				
			||||
    'id', | 
				
			||||
    'alias', | 
				
			||||
    'agent_id', | 
				
			||||
    'job_id', | 
				
			||||
    'state', | 
				
			||||
    'last_updated', | 
				
			||||
    'actions' | 
				
			||||
  ]; | 
				
			||||
 | 
				
			||||
  show_item_dialog(id: string) { | 
				
			||||
    this.data_source!.getOne(id).then(resp => { | 
				
			||||
      if (resp.status === 'ok') { | 
				
			||||
        const dialog = this.infoDialog.open(ResultInfoDialogComponent, { | 
				
			||||
          data: resp.data as ResultModel, | 
				
			||||
          width: '1000px', | 
				
			||||
        }); | 
				
			||||
 | 
				
			||||
        dialog.afterClosed().subscribe(result => { | 
				
			||||
          this.router.navigate(['.'], { relativeTo: this.route }) | 
				
			||||
        }) | 
				
			||||
      } else { | 
				
			||||
        this.openSnackBar(resp.data) | 
				
			||||
      } | 
				
			||||
    }).catch((err: HttpErrorResponse) => this.openSnackBar(err.message)) | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,32 @@ | 
				
			||||
.data-table { | 
				
			||||
    width: 100%; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
.table-container { | 
				
			||||
    margin: 50px; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
.loading-shade { | 
				
			||||
    position: absolute; | 
				
			||||
    top: 0; | 
				
			||||
    left: 0; | 
				
			||||
    bottom: 56px; | 
				
			||||
    right: 0; | 
				
			||||
    //background: rgba(0, 0, 0, 0.15); | 
				
			||||
    z-index: 1; | 
				
			||||
    display: flex; | 
				
			||||
    align-items: center; | 
				
			||||
    justify-content: center; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#refresh_btn { | 
				
			||||
    margin-left: 10px; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
.data-table-row { | 
				
			||||
    height: 30px; | 
				
			||||
} | 
				
			||||
 | 
				
			||||
.data-table-row:hover { | 
				
			||||
    background: whitesmoke; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,84 @@ | 
				
			||||
import { OnInit, Directive } from '@angular/core'; | 
				
			||||
import { HttpClient } from '@angular/common/http'; | 
				
			||||
import { ApiTableService } from '../'; | 
				
			||||
import { MatTableDataSource } from '@angular/material/table'; | 
				
			||||
import { MatDialog } from '@angular/material/dialog'; | 
				
			||||
import { ApiModel, Area } from '../models'; | 
				
			||||
import { ActivatedRoute, Router } from '@angular/router'; | 
				
			||||
import { interval } from 'rxjs'; | 
				
			||||
import { MatSnackBar, MatSnackBarConfig } from '@angular/material/snack-bar'; | 
				
			||||
 | 
				
			||||
@Directive() | 
				
			||||
export abstract class TablesComponent<T extends ApiModel> implements OnInit { | 
				
			||||
  abstract area: Area; | 
				
			||||
  data_source!: ApiTableService<T>; | 
				
			||||
  table_data!: MatTableDataSource<T>; | 
				
			||||
 | 
				
			||||
  isLoadingResults = true; | 
				
			||||
 | 
				
			||||
  constructor( | 
				
			||||
    public httpClient: HttpClient, | 
				
			||||
    public infoDialog: MatDialog, | 
				
			||||
    public route: ActivatedRoute, | 
				
			||||
    public router: Router, | 
				
			||||
    public snackBar: MatSnackBar | 
				
			||||
  ) { | 
				
			||||
    this.table_data = new MatTableDataSource; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  ngOnInit() { | 
				
			||||
    this.data_source = new ApiTableService(this.httpClient, this.area); | 
				
			||||
    this.loadTableData(); | 
				
			||||
    this.route.queryParams.subscribe(params => { | 
				
			||||
      const id = params['id'] | 
				
			||||
      const new_agent = params['new'] | 
				
			||||
      if (id) { | 
				
			||||
        this.show_item_dialog(id); | 
				
			||||
      } | 
				
			||||
      if (new_agent) { | 
				
			||||
        this.show_item_dialog(null); | 
				
			||||
      } | 
				
			||||
    }) | 
				
			||||
    //interval(10000).subscribe(_ => this.loadTableData());
 | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  async loadTableData() { | 
				
			||||
    this.isLoadingResults = true; | 
				
			||||
    //possibly needs try/catch
 | 
				
			||||
    const data = await this.data_source!.getMany(); | 
				
			||||
    this.isLoadingResults = false; | 
				
			||||
 | 
				
			||||
    if (typeof data.data !== 'string') { | 
				
			||||
      this.table_data.data = data.data | 
				
			||||
    } else { | 
				
			||||
      alert(`Error: ${data}`) | 
				
			||||
    }; | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  apply_filter(event: Event) { | 
				
			||||
    const filterValue = (event.target as HTMLInputElement).value; | 
				
			||||
    this.table_data.filter = filterValue.trim().toLowerCase(); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  deleteItem(id: string) { | 
				
			||||
    if (confirm(`Delete ${id}?`)) { | 
				
			||||
      this.data_source!.delete(id).catch(this.openSnackBar) | 
				
			||||
    } | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  openSnackBar(message: any, error: boolean = true) { | 
				
			||||
    const msg = JSON.stringify(message) | 
				
			||||
    const _config = (duration: number): MatSnackBarConfig => { | 
				
			||||
      return { | 
				
			||||
        horizontalPosition: 'right', | 
				
			||||
        verticalPosition: 'bottom', | 
				
			||||
        duration | 
				
			||||
      } | 
				
			||||
    } | 
				
			||||
    const cfg = error ? _config(0) : _config(2000) | 
				
			||||
    this.snackBar.open(msg, 'Ok', cfg); | 
				
			||||
  } | 
				
			||||
 | 
				
			||||
  abstract displayedColumns: string[]; | 
				
			||||
  abstract show_item_dialog(id: string | null): void; | 
				
			||||
} | 
				
			||||
@ -0,0 +1,3 @@ | 
				
			||||
export function epochToStr(epoch: number): string { | 
				
			||||
    return new Date(epoch * 1000).toLocaleString('en-GB') | 
				
			||||
} | 
				
			||||
@ -0,0 +1,4 @@ | 
				
			||||
export const environment = { | 
				
			||||
  production: true, | 
				
			||||
  server: "", | 
				
			||||
}; | 
				
			||||
@ -0,0 +1,17 @@ | 
				
			||||
// This file can be replaced during build by using the `fileReplacements` array.
 | 
				
			||||
// `ng build` replaces `environment.ts` with `environment.prod.ts`.
 | 
				
			||||
// The list of file replacements can be found in `angular.json`.
 | 
				
			||||
 | 
				
			||||
export const environment = { | 
				
			||||
  production: false, | 
				
			||||
  server: "http://127.0.0.1:8080", | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
/* | 
				
			||||
 * For easier debugging in development mode, you can import the following file | 
				
			||||
 * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. | 
				
			||||
 * | 
				
			||||
 * This import should be commented out in production mode because it will have a negative impact | 
				
			||||
 * on performance if an error is thrown. | 
				
			||||
 */ | 
				
			||||
// import 'zone.js/plugins/zone-error';  // Included with Angular CLI.
 | 
				
			||||
| 
		 After Width: | Height: | Size: 948 B  | 
@ -0,0 +1,18 @@ | 
				
			||||
<!doctype html> | 
				
			||||
<html lang="en"> | 
				
			||||
 | 
				
			||||
<head> | 
				
			||||
  <meta charset="utf-8"> | 
				
			||||
  <title>Fe</title> | 
				
			||||
  <meta name="viewport" content="width=device-width, initial-scale=1"> | 
				
			||||
  <link rel="icon" type="image/x-icon" href="favicon.ico"> | 
				
			||||
  <link rel="preconnect" href="https://fonts.gstatic.com"> | 
				
			||||
  <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet"> | 
				
			||||
  <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet"> | 
				
			||||
</head> | 
				
			||||
 | 
				
			||||
<body class="mat-typography"> | 
				
			||||
  <app-root></app-root> | 
				
			||||
</body> | 
				
			||||
 | 
				
			||||
</html> | 
				
			||||
@ -0,0 +1,12 @@ | 
				
			||||
import { enableProdMode } from '@angular/core'; | 
				
			||||
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; | 
				
			||||
 | 
				
			||||
import { AppModule } from './app/app.module'; | 
				
			||||
import { environment } from './environments/environment'; | 
				
			||||
 | 
				
			||||
if (environment.production) { | 
				
			||||
  enableProdMode(); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
platformBrowserDynamic().bootstrapModule(AppModule) | 
				
			||||
  .catch(err => console.error(err)); | 
				
			||||
@ -0,0 +1,53 @@ | 
				
			||||
/** | 
				
			||||
 * This file includes polyfills needed by Angular and is loaded before the app. | 
				
			||||
 * You can add your own extra polyfills to this file. | 
				
			||||
 * | 
				
			||||
 * This file is divided into 2 sections: | 
				
			||||
 *   1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. | 
				
			||||
 *   2. Application imports. Files imported after ZoneJS that should be loaded before your main | 
				
			||||
 *      file. | 
				
			||||
 * | 
				
			||||
 * The current setup is for so-called "evergreen" browsers; the last versions of browsers that | 
				
			||||
 * automatically update themselves. This includes recent versions of Safari, Chrome (including | 
				
			||||
 * Opera), Edge on the desktop, and iOS and Chrome on mobile. | 
				
			||||
 * | 
				
			||||
 * Learn more in https://angular.io/guide/browser-support
 | 
				
			||||
 */ | 
				
			||||
 | 
				
			||||
/*************************************************************************************************** | 
				
			||||
 * BROWSER POLYFILLS | 
				
			||||
 */ | 
				
			||||
 | 
				
			||||
/** | 
				
			||||
 * By default, zone.js will patch all possible macroTask and DomEvents | 
				
			||||
 * user can disable parts of macroTask/DomEvents patch by setting following flags | 
				
			||||
 * because those flags need to be set before `zone.js` being loaded, and webpack | 
				
			||||
 * will put import in the top of bundle, so user need to create a separate file | 
				
			||||
 * in this directory (for example: zone-flags.ts), and put the following flags | 
				
			||||
 * into that file, and then add the following code before importing zone.js. | 
				
			||||
 * import './zone-flags'; | 
				
			||||
 * | 
				
			||||
 * The flags allowed in zone-flags.ts are listed here. | 
				
			||||
 * | 
				
			||||
 * The following flags will work for all browsers. | 
				
			||||
 * | 
				
			||||
 * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame
 | 
				
			||||
 * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick
 | 
				
			||||
 * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames
 | 
				
			||||
 * | 
				
			||||
 *  in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js | 
				
			||||
 *  with the following flag, it will bypass `zone.js` patch for IE/Edge | 
				
			||||
 * | 
				
			||||
 *  (window as any).__Zone_enable_cross_context_check = true; | 
				
			||||
 * | 
				
			||||
 */ | 
				
			||||
 | 
				
			||||
/*************************************************************************************************** | 
				
			||||
 * Zone JS is required by default for Angular itself. | 
				
			||||
 */ | 
				
			||||
import 'zone.js';  // Included with Angular CLI.
 | 
				
			||||
 | 
				
			||||
 | 
				
			||||
/*************************************************************************************************** | 
				
			||||
 * APPLICATION IMPORTS | 
				
			||||
 */ | 
				
			||||
@ -0,0 +1,4 @@ | 
				
			||||
/* You can add global styles to this file, and also import other style files */ | 
				
			||||
 | 
				
			||||
html, body { height: 100%; } | 
				
			||||
body { margin: 0; font-family: Roboto, "Helvetica Neue", sans-serif; } | 
				
			||||
@ -0,0 +1,26 @@ | 
				
			||||
// This file is required by karma.conf.js and loads recursively all the .spec and framework files
 | 
				
			||||
 | 
				
			||||
import 'zone.js/testing'; | 
				
			||||
import { getTestBed } from '@angular/core/testing'; | 
				
			||||
import { | 
				
			||||
  BrowserDynamicTestingModule, | 
				
			||||
  platformBrowserDynamicTesting | 
				
			||||
} from '@angular/platform-browser-dynamic/testing'; | 
				
			||||
 | 
				
			||||
declare const require: { | 
				
			||||
  context(path: string, deep?: boolean, filter?: RegExp): { | 
				
			||||
    <T>(id: string): T; | 
				
			||||
    keys(): string[]; | 
				
			||||
  }; | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
// First, initialize the Angular testing environment.
 | 
				
			||||
getTestBed().initTestEnvironment( | 
				
			||||
  BrowserDynamicTestingModule, | 
				
			||||
  platformBrowserDynamicTesting(), | 
				
			||||
); | 
				
			||||
 | 
				
			||||
// Then we find all the tests.
 | 
				
			||||
const context = require.context('./', true, /\.spec\.ts$/); | 
				
			||||
// And load the modules.
 | 
				
			||||
context.keys().map(context); | 
				
			||||
@ -0,0 +1,15 @@ | 
				
			||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ | 
				
			||||
{ | 
				
			||||
  "extends": "./tsconfig.json", | 
				
			||||
  "compilerOptions": { | 
				
			||||
    "outDir": "./out-tsc/app", | 
				
			||||
    "types": [] | 
				
			||||
  }, | 
				
			||||
  "files": [ | 
				
			||||
    "src/main.ts", | 
				
			||||
    "src/polyfills.ts" | 
				
			||||
  ], | 
				
			||||
  "include": [ | 
				
			||||
    "src/**/*.d.ts" | 
				
			||||
  ] | 
				
			||||
} | 
				
			||||
@ -0,0 +1,32 @@ | 
				
			||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ | 
				
			||||
{ | 
				
			||||
  "compileOnSave": false, | 
				
			||||
  "compilerOptions": { | 
				
			||||
    "baseUrl": "./", | 
				
			||||
    "outDir": "./dist/out-tsc", | 
				
			||||
    "forceConsistentCasingInFileNames": true, | 
				
			||||
    "strict": true, | 
				
			||||
    "noImplicitOverride": true, | 
				
			||||
    "noPropertyAccessFromIndexSignature": true, | 
				
			||||
    "noImplicitReturns": true, | 
				
			||||
    "noFallthroughCasesInSwitch": true, | 
				
			||||
    "sourceMap": true, | 
				
			||||
    "declaration": false, | 
				
			||||
    "downlevelIteration": true, | 
				
			||||
    "experimentalDecorators": true, | 
				
			||||
    "moduleResolution": "node", | 
				
			||||
    "importHelpers": true, | 
				
			||||
    "target": "es2017", | 
				
			||||
    "module": "es2020", | 
				
			||||
    "lib": [ | 
				
			||||
      "es2020", | 
				
			||||
      "dom" | 
				
			||||
    ] | 
				
			||||
  }, | 
				
			||||
  "angularCompilerOptions": { | 
				
			||||
    "enableI18nLegacyMessageIdFormat": false, | 
				
			||||
    "strictInjectionParameters": true, | 
				
			||||
    "strictInputAccessModifiers": true, | 
				
			||||
    "strictTemplates": true | 
				
			||||
  } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,18 @@ | 
				
			||||
/* To learn more about this file see: https://angular.io/config/tsconfig. */ | 
				
			||||
{ | 
				
			||||
  "extends": "./tsconfig.json", | 
				
			||||
  "compilerOptions": { | 
				
			||||
    "outDir": "./out-tsc/spec", | 
				
			||||
    "types": [ | 
				
			||||
      "jasmine" | 
				
			||||
    ] | 
				
			||||
  }, | 
				
			||||
  "files": [ | 
				
			||||
    "src/test.ts", | 
				
			||||
    "src/polyfills.ts" | 
				
			||||
  ], | 
				
			||||
  "include": [ | 
				
			||||
    "src/**/*.spec.ts", | 
				
			||||
    "src/**/*.d.ts" | 
				
			||||
  ] | 
				
			||||
} | 
				
			||||
@ -0,0 +1,98 @@ | 
				
			||||
mod error; | 
				
			||||
 | 
				
			||||
use crate::{process_cmd, Args}; | 
				
			||||
use actix_cors::Cors; | 
				
			||||
use actix_web::{get, middleware::Logger, post, web, App, HttpResponse, HttpServer, Responder}; | 
				
			||||
use error::Error; | 
				
			||||
use futures_util::StreamExt; | 
				
			||||
use rust_embed::RustEmbed; | 
				
			||||
use std::borrow::Cow; | 
				
			||||
use structopt::StructOpt; | 
				
			||||
use u_lib::{api::ClientHandler, unwrap_enum}; | 
				
			||||
 | 
				
			||||
#[derive(RustEmbed)] | 
				
			||||
#[folder = "./src/server/fe/dist/fe/"] | 
				
			||||
struct Files; | 
				
			||||
 | 
				
			||||
impl Files { | 
				
			||||
    pub fn get_static(path: impl AsRef<str>) -> Option<&'static [u8]> { | 
				
			||||
        let file = Self::get(path.as_ref())?.data; | 
				
			||||
        Some(unwrap_enum!(file, Cow::Borrowed)) | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
async fn spa_main() -> impl Responder { | 
				
			||||
    let index = Files::get_static("index.html").unwrap(); | 
				
			||||
    HttpResponse::Ok().body(index) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[get("/core/{path}")] | 
				
			||||
async fn resources_adapter(path: web::Path<(String,)>) -> impl Responder { | 
				
			||||
    let path = path.into_inner().0; | 
				
			||||
    let mimetype = mime_guess::from_path(&path).first_or_octet_stream(); | 
				
			||||
 | 
				
			||||
    match Files::get_static(path) { | 
				
			||||
        Some(data) => HttpResponse::Ok() | 
				
			||||
            .content_type(mimetype.to_string()) | 
				
			||||
            .body(data), | 
				
			||||
        None => HttpResponse::NotFound().finish(), | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[post("/cmd/")] | 
				
			||||
async fn send_cmd( | 
				
			||||
    mut body: web::Payload, | 
				
			||||
    client: web::Data<ClientHandler>, | 
				
			||||
) -> Result<impl Responder, Error> { | 
				
			||||
    let mut bytes = web::BytesMut::new(); | 
				
			||||
 | 
				
			||||
    while let Some(item) = body.next().await { | 
				
			||||
        bytes.extend_from_slice( | 
				
			||||
            &item.map_err(|e| Error::JustError(format!("payload loading failure: {e}")))?, | 
				
			||||
        ); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    let cmd = String::from_utf8(bytes.to_vec()) | 
				
			||||
        .map_err(|_| Error::JustError("cmd contains non-utf8 data".to_string()))?; | 
				
			||||
    let mut cmd = shlex::split(&cmd).ok_or(Error::JustError("argparse failed".to_string()))?; | 
				
			||||
 | 
				
			||||
    info!("cmd: {:?}", cmd); | 
				
			||||
    cmd.insert(0, String::from("u_panel")); | 
				
			||||
 | 
				
			||||
    let parsed_cmd = Args::from_iter_safe(cmd)?; | 
				
			||||
    let result = process_cmd(client.as_ref().clone(), parsed_cmd).await; | 
				
			||||
    let result_string = result.to_string(); | 
				
			||||
 | 
				
			||||
    let response = if result.is_ok() { | 
				
			||||
        HttpResponse::Ok().body(result_string) | 
				
			||||
    } else if result.is_err() { | 
				
			||||
        HttpResponse::BadRequest().body(result_string) | 
				
			||||
    } else { | 
				
			||||
        unreachable!() | 
				
			||||
    }; | 
				
			||||
 | 
				
			||||
    Ok(response) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub async fn serve(client: ClientHandler) -> anyhow::Result<()> { | 
				
			||||
    info!("Connecting to u_server..."); | 
				
			||||
    client.ping().await?; | 
				
			||||
 | 
				
			||||
    let addr = "127.0.0.1:8080"; | 
				
			||||
    info!("Connected, instanciating u_panel at http://{}", addr); | 
				
			||||
 | 
				
			||||
    HttpServer::new(move || { | 
				
			||||
        App::new() | 
				
			||||
            .wrap(Logger::default()) | 
				
			||||
            .wrap(Cors::permissive()) | 
				
			||||
            .app_data(web::Data::new(client.clone())) | 
				
			||||
            .service(send_cmd) | 
				
			||||
            .service(resources_adapter) | 
				
			||||
            .service(web::resource("/").to(spa_main)) | 
				
			||||
            .service(web::resource("/{_}").to(spa_main)) | 
				
			||||
    }) | 
				
			||||
    .bind(addr)? | 
				
			||||
    .run() | 
				
			||||
    .await?; | 
				
			||||
    Ok(()) | 
				
			||||
} | 
				
			||||
@ -0,0 +1,59 @@ | 
				
			||||
use diesel::result::Error as DslError; | 
				
			||||
use thiserror::Error; | 
				
			||||
use warp::{ | 
				
			||||
    http::StatusCode, | 
				
			||||
    reject::Reject, | 
				
			||||
    reply::{with_status, Response}, | 
				
			||||
    Reply, | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
#[derive(Error, Debug)] | 
				
			||||
pub enum Error { | 
				
			||||
    #[error("Error processing {0}")] | 
				
			||||
    ProcessingError(String), | 
				
			||||
 | 
				
			||||
    #[error(transparent)] | 
				
			||||
    DBError(#[from] DslError), | 
				
			||||
 | 
				
			||||
    #[error("DB error: {0}")] | 
				
			||||
    DBErrorCtx(String), | 
				
			||||
 | 
				
			||||
    #[error("General error: {0}")] | 
				
			||||
    Other(String), | 
				
			||||
} | 
				
			||||
 | 
				
			||||
impl Reject for Error {} | 
				
			||||
 | 
				
			||||
pub struct RejResponse { | 
				
			||||
    message: String, | 
				
			||||
    status: StatusCode, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
impl RejResponse { | 
				
			||||
    pub fn not_found(msg: impl Into<String>) -> Self { | 
				
			||||
        Self { | 
				
			||||
            message: msg.into(), | 
				
			||||
            status: StatusCode::NOT_FOUND, | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub fn bad_request(msg: impl Into<String>) -> Self { | 
				
			||||
        Self { | 
				
			||||
            message: msg.into(), | 
				
			||||
            status: StatusCode::BAD_REQUEST, | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub fn internal() -> Self { | 
				
			||||
        Self { | 
				
			||||
            message: "INTERNAL_SERVER_ERROR".to_string(), | 
				
			||||
            status: StatusCode::INTERNAL_SERVER_ERROR, | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
impl Reply for RejResponse { | 
				
			||||
    fn into_response(self) -> Response { | 
				
			||||
        with_status(self.message, self.status).into_response() | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -1,87 +0,0 @@ | 
				
			||||
use crate::handlers::Endpoints; | 
				
			||||
use serde::de::DeserializeOwned; | 
				
			||||
use std::env; | 
				
			||||
use u_lib::{ | 
				
			||||
    messaging::{AsMsg, BaseMessage}, | 
				
			||||
    models::*, | 
				
			||||
}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
use warp::{body, Filter, Rejection, Reply}; | 
				
			||||
 | 
				
			||||
fn get_content<M>() -> impl Filter<Extract = (BaseMessage<'static, M>,), Error = Rejection> + Clone | 
				
			||||
where | 
				
			||||
    M: AsMsg + Sync + Send + DeserializeOwned + 'static, | 
				
			||||
{ | 
				
			||||
    body::content_length_limit(1024 * 64).and(body::json::<BaseMessage<M>>()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub fn make_filters() -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { | 
				
			||||
    let infallible_none = |_| async { Ok::<(Option<Uuid>,), std::convert::Infallible>((None,)) }; | 
				
			||||
 | 
				
			||||
    let get_agents = warp::get() | 
				
			||||
        .and(warp::path("get_agents")) | 
				
			||||
        .and( | 
				
			||||
            warp::path::param::<Uuid>() | 
				
			||||
                .map(Some) | 
				
			||||
                .or_else(infallible_none), | 
				
			||||
        ) | 
				
			||||
        .and_then(Endpoints::get_agents); | 
				
			||||
 | 
				
			||||
    let upload_jobs = warp::post() | 
				
			||||
        .and(warp::path("upload_jobs")) | 
				
			||||
        .and(get_content::<Vec<JobMeta>>()) | 
				
			||||
        .and_then(Endpoints::upload_jobs); | 
				
			||||
 | 
				
			||||
    let get_jobs = warp::get() | 
				
			||||
        .and(warp::path("get_jobs")) | 
				
			||||
        .and( | 
				
			||||
            warp::path::param::<Uuid>() | 
				
			||||
                .map(Some) | 
				
			||||
                .or_else(infallible_none), | 
				
			||||
        ) | 
				
			||||
        .and_then(Endpoints::get_jobs); | 
				
			||||
 | 
				
			||||
    let get_agent_jobs = warp::get() | 
				
			||||
        .and(warp::path("get_agent_jobs")) | 
				
			||||
        .and( | 
				
			||||
            warp::path::param::<Uuid>() | 
				
			||||
                .map(Some) | 
				
			||||
                .or_else(infallible_none), | 
				
			||||
        ) | 
				
			||||
        .and_then(|uid| Endpoints::get_agent_jobs(uid)); | 
				
			||||
 | 
				
			||||
    let get_personal_jobs = warp::get() | 
				
			||||
        .and(warp::path("get_personal_jobs")) | 
				
			||||
        .and(warp::path::param::<Uuid>().map(Some)) | 
				
			||||
        .and_then(|uid| Endpoints::get_personal_jobs(uid)); | 
				
			||||
 | 
				
			||||
    let del = warp::get() | 
				
			||||
        .and(warp::path("del")) | 
				
			||||
        .and(warp::path::param::<Uuid>()) | 
				
			||||
        .and_then(Endpoints::del); | 
				
			||||
 | 
				
			||||
    let set_jobs = warp::post() | 
				
			||||
        .and(warp::path("set_jobs")) | 
				
			||||
        .and(warp::path::param::<Uuid>()) | 
				
			||||
        .and(get_content::<Vec<String>>()) | 
				
			||||
        .and_then(Endpoints::set_jobs); | 
				
			||||
 | 
				
			||||
    let report = warp::post() | 
				
			||||
        .and(warp::path("report")) | 
				
			||||
        .and(get_content::<Vec<ExecResult>>().and_then(Endpoints::report)); | 
				
			||||
 | 
				
			||||
    let auth_token = format!("Bearer {}", env::var("ADMIN_AUTH_TOKEN").unwrap()).into_boxed_str(); | 
				
			||||
    let auth_header = warp::header::exact("authorization", Box::leak(auth_token)); | 
				
			||||
 | 
				
			||||
    let auth_zone = (get_agents | 
				
			||||
        .or(get_jobs) | 
				
			||||
        .or(upload_jobs) | 
				
			||||
        .or(del) | 
				
			||||
        .or(set_jobs) | 
				
			||||
        .or(get_agent_jobs)) | 
				
			||||
    .and(auth_header); | 
				
			||||
 | 
				
			||||
    let agent_zone = get_jobs.clone().or(get_personal_jobs).or(report); | 
				
			||||
 | 
				
			||||
    auth_zone.or(agent_zone) | 
				
			||||
} | 
				
			||||
@ -1,171 +1,162 @@ | 
				
			||||
use crate::db::UDB; | 
				
			||||
use diesel::SaveChangesDsl; | 
				
			||||
use hyper::Body; | 
				
			||||
use serde::Serialize; | 
				
			||||
use crate::error::Error; | 
				
			||||
use u_lib::{ | 
				
			||||
    messaging::{AsMsg, BaseMessage}, | 
				
			||||
    models::{Agent, AgentState, AssignedJob, ExecResult, JobMeta, JobState}, | 
				
			||||
    ULocalError, | 
				
			||||
    messaging::{AsMsg, BaseMessage, Reportable}, | 
				
			||||
    models::*, | 
				
			||||
    utils::OneOrVec, | 
				
			||||
}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
use warp::{ | 
				
			||||
    http::{Response, StatusCode}, | 
				
			||||
    Rejection, Reply, | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
pub fn build_response<S: Into<Body>>(code: StatusCode, body: S) -> Response<Body> { | 
				
			||||
    Response::builder().status(code).body(body.into()).unwrap() | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub fn build_ok<S: Into<Body>>(body: S) -> Response<Body> { | 
				
			||||
    build_response(StatusCode::OK, body) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub fn build_err<S: ToString>(body: S) -> Response<Body> { | 
				
			||||
    build_response(StatusCode::BAD_REQUEST, body.to_string()) | 
				
			||||
} | 
				
			||||
use warp::Rejection; | 
				
			||||
 | 
				
			||||
pub fn build_message<M: AsMsg + Serialize>(m: M) -> Response<Body> { | 
				
			||||
    warp::reply::json(&m.as_message()).into_response() | 
				
			||||
} | 
				
			||||
type EndpResult<T> = Result<T, Rejection>; | 
				
			||||
 | 
				
			||||
pub struct Endpoints; | 
				
			||||
 | 
				
			||||
#[cfg_attr(test, automock)] | 
				
			||||
impl Endpoints { | 
				
			||||
    pub async fn add_agent(msg: Agent) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: add_agent"); | 
				
			||||
        UDB::lock_db() | 
				
			||||
            .insert_agent(&msg) | 
				
			||||
            .map(|_| build_ok("")) | 
				
			||||
            .or_else(|e| Ok(build_err(e))) | 
				
			||||
    pub async fn add_agent(msg: Agent) -> EndpResult<()> { | 
				
			||||
        UDB::lock_db().insert_agent(&msg).map_err(From::from) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn get_agents(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: get_agents"); | 
				
			||||
        UDB::lock_db() | 
				
			||||
            .get_agents(uid) | 
				
			||||
            .map(|m| build_message(m)) | 
				
			||||
            .or_else(|e| Ok(build_err(e))) | 
				
			||||
    pub async fn get_agents(uid: Option<Uuid>) -> EndpResult<Vec<Agent>> { | 
				
			||||
        UDB::lock_db().get_agents(uid).map_err(From::from) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn get_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: get_jobs"); | 
				
			||||
        UDB::lock_db() | 
				
			||||
            .get_jobs(uid) | 
				
			||||
            .map(|m| build_message(m)) | 
				
			||||
            .or_else(|e| Ok(build_err(e))) | 
				
			||||
    pub async fn get_jobs(uid: Option<Uuid>) -> EndpResult<Vec<JobMeta>> { | 
				
			||||
        UDB::lock_db().get_jobs(uid).map_err(From::from) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn get_agent_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: get_agent_jobs"); | 
				
			||||
    pub async fn get_agent_jobs(uid: Option<Uuid>) -> EndpResult<Vec<AssignedJob>> { | 
				
			||||
        UDB::lock_db() | 
				
			||||
            .get_exact_jobs(uid, false) | 
				
			||||
            .map(|m| build_message(m)) | 
				
			||||
            .or_else(|e| Ok(build_err(e))) | 
				
			||||
            .map_err(From::from) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn get_personal_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: get_personal_jobs"); | 
				
			||||
        let agents = UDB::lock_db().get_agents(uid).unwrap(); | 
				
			||||
        if agents.len() == 0 { | 
				
			||||
            let db = UDB::lock_db(); | 
				
			||||
            db.insert_agent(&Agent::with_id(uid.unwrap())).unwrap(); | 
				
			||||
            let job = db.find_job_by_alias("agent_hello").unwrap(); | 
				
			||||
            if let Err(e) = db.set_jobs_for_agent(&uid.unwrap(), &[job.id]) { | 
				
			||||
                return Ok(build_err(e)); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        let result = UDB::lock_db().get_exact_jobs(uid, true); | 
				
			||||
        match result { | 
				
			||||
            Ok(r) => { | 
				
			||||
    pub async fn get_personal_jobs(uid: Uuid) -> EndpResult<Vec<AssignedJob>> { | 
				
			||||
        let db = UDB::lock_db(); | 
				
			||||
                for j in r.iter() { | 
				
			||||
                    db.update_job_status(j.id, JobState::Running).unwrap(); | 
				
			||||
                } | 
				
			||||
                Ok(build_message(r)) | 
				
			||||
            } | 
				
			||||
            Err(e) => Ok(build_err(e)), | 
				
			||||
        let mut agents = db.get_agents(Some(uid))?; | 
				
			||||
        if agents.is_empty() { | 
				
			||||
            let new_agent = Agent::with_id(uid); | 
				
			||||
            db.insert_agent(&new_agent)?; | 
				
			||||
            let job = db | 
				
			||||
                .find_job_by_alias("agent_hello")? | 
				
			||||
                .expect("agent_hello job not found"); | 
				
			||||
            db.set_jobs_for_agent(&uid, &[job.id])?; | 
				
			||||
        } else { | 
				
			||||
            let mut agent = agents.pop().unwrap(); | 
				
			||||
            agent.touch(); | 
				
			||||
            db.update_agent(&agent)?; | 
				
			||||
        } | 
				
			||||
        let result = db.get_exact_jobs(Some(uid), true)?; | 
				
			||||
 | 
				
			||||
        for j in result.iter() { | 
				
			||||
            db.update_job_status(j.id, JobState::Running)?; | 
				
			||||
        } | 
				
			||||
        Ok(result) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn upload_jobs( | 
				
			||||
        msg: BaseMessage<'static, Vec<JobMeta>>, | 
				
			||||
    ) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: upload_jobs"); | 
				
			||||
    pub async fn upload_jobs(msg: BaseMessage<'static, Vec<JobMeta>>) -> EndpResult<Vec<Uuid>> { | 
				
			||||
        UDB::lock_db() | 
				
			||||
            .insert_jobs(&msg.into_inner()) | 
				
			||||
            .map(|_| build_ok("")) | 
				
			||||
            .or_else(|e| Ok(build_err(e))) | 
				
			||||
            .map_err(From::from) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn del(uid: Uuid) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: del"); | 
				
			||||
    pub async fn del(uid: Uuid) -> EndpResult<usize> { | 
				
			||||
        let db = UDB::lock_db(); | 
				
			||||
        let del_fns = &[UDB::del_agents, UDB::del_jobs, UDB::del_results]; | 
				
			||||
        for del_fn in del_fns { | 
				
			||||
            let affected = del_fn(&db, &vec![uid]).unwrap(); | 
				
			||||
            let affected = del_fn(&db, &[uid])?; | 
				
			||||
            if affected > 0 { | 
				
			||||
                return Ok(build_message(affected as i32)); | 
				
			||||
                return Ok(affected); | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
        Ok(build_message(0)) | 
				
			||||
        Ok(0) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn set_jobs( | 
				
			||||
        agent_uid: Uuid, | 
				
			||||
        msg: BaseMessage<'static, Vec<String>>, | 
				
			||||
    ) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: set_jobs_by_alias, agent: {}", agent_uid); | 
				
			||||
        let jobs: Result<Vec<Uuid>, ULocalError> = msg | 
				
			||||
            .into_inner() | 
				
			||||
    ) -> EndpResult<Vec<Uuid>> { | 
				
			||||
        msg.into_inner() | 
				
			||||
            .into_iter() | 
				
			||||
            .map(|ident| { | 
				
			||||
                info!("hnd: set_jobs_by_alias, job: {}", ident); | 
				
			||||
                Uuid::parse_str(&ident) | 
				
			||||
                    .or_else(|_| UDB::lock_db().find_job_by_alias(&ident).map(|j| j.id)) | 
				
			||||
            }) | 
				
			||||
            .collect(); | 
				
			||||
        match jobs { | 
				
			||||
            Ok(j) => UDB::lock_db() | 
				
			||||
                .set_jobs_for_agent(&agent_uid, &j) | 
				
			||||
                .map(|assigned_uids| build_message(assigned_uids)) | 
				
			||||
                .or_else(|e| Ok(build_err(e))), | 
				
			||||
            Err(e) => Ok(build_err(e)), | 
				
			||||
                Uuid::parse_str(&ident).or_else(|_| { | 
				
			||||
                    let job_from_db = UDB::lock_db().find_job_by_alias(&ident); | 
				
			||||
                    match job_from_db { | 
				
			||||
                        Ok(job) => match job { | 
				
			||||
                            Some(j) => Ok(j.id), | 
				
			||||
                            None => Err(Error::ProcessingError(format!("unknown ident {ident}"))), | 
				
			||||
                        }, | 
				
			||||
                        Err(e) => Err(e), | 
				
			||||
                    } | 
				
			||||
                }) | 
				
			||||
            }) | 
				
			||||
            .collect::<Result<Vec<Uuid>, Error>>() | 
				
			||||
            .and_then(|j| UDB::lock_db().set_jobs_for_agent(&agent_uid, &j)) | 
				
			||||
            .map_err(From::from) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn report( | 
				
			||||
        msg: BaseMessage<'static, Vec<ExecResult>>, | 
				
			||||
    ) -> Result<Response<Body>, Rejection> { | 
				
			||||
        info!("hnd: report"); | 
				
			||||
    pub async fn report<Data: OneOrVec<Reportable> + AsMsg + 'static>( | 
				
			||||
        msg: BaseMessage<'static, Data>, | 
				
			||||
    ) -> EndpResult<()> { | 
				
			||||
        let id = msg.id; | 
				
			||||
        let mut failed = vec![]; | 
				
			||||
        for entry in msg.into_inner() { | 
				
			||||
        for entry in msg.into_inner().into_vec() { | 
				
			||||
            match entry { | 
				
			||||
                ExecResult::Assigned(res) => { | 
				
			||||
                    if id != res.agent_id { | 
				
			||||
                Reportable::Assigned(mut result) => { | 
				
			||||
                    let result_agent_id = &result.agent_id; | 
				
			||||
                    if id != *result_agent_id { | 
				
			||||
                        warn!("Ids are not equal! actual id: {id}, id from job: {result_agent_id}"); | 
				
			||||
                        continue; | 
				
			||||
                    } | 
				
			||||
                    let db = UDB::lock_db(); | 
				
			||||
                    if let Err(e) = res | 
				
			||||
                        .save_changes::<AssignedJob>(&db.conn) | 
				
			||||
                        .map_err(ULocalError::from) | 
				
			||||
                    { | 
				
			||||
                        failed.push(e.to_string()) | 
				
			||||
                    result.state = JobState::Finished; | 
				
			||||
                    result.touch(); | 
				
			||||
                    match result.exec_type { | 
				
			||||
                        JobType::Init => match &result.result { | 
				
			||||
                            Some(rbytes) => { | 
				
			||||
                                let mut agent: Agent = match serde_json::from_slice(&rbytes) { | 
				
			||||
                                    Ok(a) => a, | 
				
			||||
                                    Err(e) => { | 
				
			||||
                                        warn!("Error deserializing agent from {id}: {e}"); | 
				
			||||
                                        continue; | 
				
			||||
                                    } | 
				
			||||
                                }; | 
				
			||||
                                agent.state = AgentState::Active; | 
				
			||||
                                Self::add_agent(agent).await?; | 
				
			||||
                            } | 
				
			||||
                            None => warn!("Empty agent data"), | 
				
			||||
                        }, | 
				
			||||
                        JobType::Shell => (), | 
				
			||||
                        JobType::Terminate => todo!(), | 
				
			||||
                        JobType::Update => todo!(), | 
				
			||||
                    } | 
				
			||||
                ExecResult::Agent(mut a) => { | 
				
			||||
                    a.state = AgentState::Active; | 
				
			||||
                    Self::add_agent(a).await?; | 
				
			||||
                    UDB::lock_db().update_result(&result)?; | 
				
			||||
                } | 
				
			||||
                ExecResult::Dummy => (), | 
				
			||||
                Reportable::Error(e) => { | 
				
			||||
                    warn!("{} reported an error: {}", id, e); | 
				
			||||
                } | 
				
			||||
                Reportable::Dummy => (), | 
				
			||||
            } | 
				
			||||
        if failed.len() > 0 { | 
				
			||||
            let err_msg = ULocalError::ProcessingError(failed.join(", ")); | 
				
			||||
            return Ok(build_err(err_msg)); | 
				
			||||
        } | 
				
			||||
        Ok(build_ok("")) | 
				
			||||
        Ok(()) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn update_agent(agent: BaseMessage<'static, Agent>) -> EndpResult<()> { | 
				
			||||
        UDB::lock_db().update_agent(&agent.into_inner())?; | 
				
			||||
        Ok(()) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn update_job(job: BaseMessage<'static, JobMeta>) -> EndpResult<()> { | 
				
			||||
        UDB::lock_db().update_job(&job.into_inner())?; | 
				
			||||
        Ok(()) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn update_assigned_job( | 
				
			||||
        assigned: BaseMessage<'static, AssignedJob>, | 
				
			||||
    ) -> EndpResult<()> { | 
				
			||||
        UDB::lock_db().update_result(&assigned.into_inner())?; | 
				
			||||
        Ok(()) | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn download(_file_uid: String) -> EndpResult<Vec<u8>> { | 
				
			||||
        todo!() | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -1,6 +1,11 @@ | 
				
			||||
use u_server_lib::serve; | 
				
			||||
 | 
				
			||||
#[macro_use] | 
				
			||||
extern crate tracing; | 
				
			||||
 | 
				
			||||
#[tokio::main] | 
				
			||||
async fn main() { | 
				
			||||
    serve().await; | 
				
			||||
    if let Err(e) = serve().await { | 
				
			||||
        error!("U_SERVER error: {}", e); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
@ -1,117 +1,267 @@ | 
				
			||||
#[macro_use] | 
				
			||||
extern crate log; | 
				
			||||
extern crate tracing; | 
				
			||||
 | 
				
			||||
#[cfg(test)] | 
				
			||||
#[macro_use] | 
				
			||||
extern crate mockall; | 
				
			||||
#[macro_use] | 
				
			||||
extern crate mockall_double; | 
				
			||||
extern crate rstest; | 
				
			||||
 | 
				
			||||
// because of linking errors
 | 
				
			||||
// due to linking errors
 | 
				
			||||
extern crate openssl; | 
				
			||||
#[macro_use] | 
				
			||||
// don't touch anything
 | 
				
			||||
extern crate diesel; | 
				
			||||
//
 | 
				
			||||
// in this block
 | 
				
			||||
 | 
				
			||||
mod db; | 
				
			||||
mod filters; | 
				
			||||
mod error; | 
				
			||||
mod handlers; | 
				
			||||
 | 
				
			||||
use db::UDB; | 
				
			||||
use filters::make_filters; | 
				
			||||
use u_lib::{config::MASTER_PORT, models::*, utils::init_env}; | 
				
			||||
use warp::Filter; | 
				
			||||
use error::{Error as ServerError, RejResponse}; | 
				
			||||
use serde::{de::DeserializeOwned, Deserialize}; | 
				
			||||
use std::{convert::Infallible, path::PathBuf}; | 
				
			||||
use u_lib::{ | 
				
			||||
    config::MASTER_PORT, | 
				
			||||
    logging::init_logger, | 
				
			||||
    messaging::{AsMsg, BaseMessage, Reportable}, | 
				
			||||
    models::*, | 
				
			||||
    utils::load_env, | 
				
			||||
}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
use warp::{ | 
				
			||||
    body, | 
				
			||||
    log::{custom, Info}, | 
				
			||||
    reply::{json, reply, Json, Response}, | 
				
			||||
    Filter, Rejection, Reply, | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
use crate::db::UDB; | 
				
			||||
use crate::handlers::Endpoints; | 
				
			||||
 | 
				
			||||
#[derive(Deserialize)] | 
				
			||||
struct ServEnv { | 
				
			||||
    admin_auth_token: String, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn get_content<M>() -> impl Filter<Extract = (BaseMessage<'static, M>,), Error = Rejection> + Clone | 
				
			||||
where | 
				
			||||
    M: AsMsg + Sync + Send + DeserializeOwned + 'static, | 
				
			||||
{ | 
				
			||||
    body::content_length_limit(1024 * 64).and(body::json::<BaseMessage<M>>()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn into_message<M: AsMsg>(msg: M) -> Json { | 
				
			||||
    json(&msg.as_message()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub fn init_endpoints( | 
				
			||||
    auth_token: &str, | 
				
			||||
) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { | 
				
			||||
    let path = |p: &'static str| warp::post().and(warp::path(p)); | 
				
			||||
    let infallible_none = |_| async { Ok::<_, Infallible>((None::<Uuid>,)) }; | 
				
			||||
 | 
				
			||||
    let get_agents = path("get_agents") | 
				
			||||
        .and( | 
				
			||||
            warp::path::param::<Uuid>() | 
				
			||||
                .map(Some) | 
				
			||||
                .or_else(infallible_none), | 
				
			||||
        ) | 
				
			||||
        .and_then(Endpoints::get_agents) | 
				
			||||
        .map(into_message); | 
				
			||||
 | 
				
			||||
    let upload_jobs = path("upload_jobs") | 
				
			||||
        .and(get_content::<Vec<JobMeta>>()) | 
				
			||||
        .and_then(Endpoints::upload_jobs) | 
				
			||||
        .map(into_message); | 
				
			||||
 | 
				
			||||
    let get_jobs = path("get_jobs") | 
				
			||||
        .and( | 
				
			||||
            warp::path::param::<Uuid>() | 
				
			||||
                .map(Some) | 
				
			||||
                .or_else(infallible_none), | 
				
			||||
        ) | 
				
			||||
        .and_then(Endpoints::get_jobs) | 
				
			||||
        .map(into_message); | 
				
			||||
 | 
				
			||||
    let get_agent_jobs = path("get_agent_jobs") | 
				
			||||
        .and( | 
				
			||||
            warp::path::param::<Uuid>() | 
				
			||||
                .map(Some) | 
				
			||||
                .or_else(infallible_none), | 
				
			||||
        ) | 
				
			||||
        .and_then(Endpoints::get_agent_jobs) | 
				
			||||
        .map(into_message); | 
				
			||||
 | 
				
			||||
    let get_personal_jobs = path("get_personal_jobs") | 
				
			||||
        .and(warp::path::param::<Uuid>()) | 
				
			||||
        .and_then(Endpoints::get_personal_jobs) | 
				
			||||
        .map(into_message); | 
				
			||||
 | 
				
			||||
    let del = path("del") | 
				
			||||
        .and(warp::path::param::<Uuid>()) | 
				
			||||
        .and_then(Endpoints::del) | 
				
			||||
        .map(ok); | 
				
			||||
 | 
				
			||||
    let set_jobs = path("set_jobs") | 
				
			||||
        .and(warp::path::param::<Uuid>()) | 
				
			||||
        .and(get_content::<Vec<String>>()) | 
				
			||||
        .and_then(Endpoints::set_jobs) | 
				
			||||
        .map(into_message); | 
				
			||||
 | 
				
			||||
const LOGFILE: &str = "u_server.log"; | 
				
			||||
    let report = path("report") | 
				
			||||
        .and(get_content::<Vec<Reportable>>()) | 
				
			||||
        .and_then(Endpoints::report) | 
				
			||||
        .map(ok); | 
				
			||||
 | 
				
			||||
fn prefill_jobs() { | 
				
			||||
    let update_agent = path("update_agent") | 
				
			||||
        .and(get_content::<Agent>()) | 
				
			||||
        .and_then(Endpoints::update_agent) | 
				
			||||
        .map(ok); | 
				
			||||
 | 
				
			||||
    let update_job = path("update_job") | 
				
			||||
        .and(get_content::<JobMeta>()) | 
				
			||||
        .and_then(Endpoints::update_job) | 
				
			||||
        .map(ok); | 
				
			||||
 | 
				
			||||
    let update_assigned_job = path("update_result") | 
				
			||||
        .and(get_content::<AssignedJob>()) | 
				
			||||
        .and_then(Endpoints::update_assigned_job) | 
				
			||||
        .map(ok); | 
				
			||||
 | 
				
			||||
    let download = path("download") | 
				
			||||
        .and(warp::path::param::<String>()) | 
				
			||||
        .and_then(Endpoints::download) | 
				
			||||
        .map(ok); | 
				
			||||
 | 
				
			||||
    let ping = path("ping").map(reply); | 
				
			||||
 | 
				
			||||
    let auth_token = format!("Bearer {auth_token}",).into_boxed_str(); | 
				
			||||
    let auth_header = warp::header::exact("authorization", Box::leak(auth_token)); | 
				
			||||
 | 
				
			||||
    let auth_zone = (get_agents | 
				
			||||
        .or(get_jobs) | 
				
			||||
        .or(upload_jobs) | 
				
			||||
        .or(del) | 
				
			||||
        .or(set_jobs) | 
				
			||||
        .or(get_agent_jobs) | 
				
			||||
        .or(update_agent.or(update_job).or(update_assigned_job)) | 
				
			||||
        .or(download) | 
				
			||||
        .or(ping)) | 
				
			||||
    .and(auth_header); | 
				
			||||
 | 
				
			||||
    let agent_zone = get_jobs.or(get_personal_jobs).or(report).or(download); | 
				
			||||
 | 
				
			||||
    auth_zone.or(agent_zone) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
pub fn preload_jobs() -> Result<(), ServerError> { | 
				
			||||
    let job_alias = "agent_hello"; | 
				
			||||
    let if_job_exists = UDB::lock_db().find_job_by_alias(job_alias)?; | 
				
			||||
    if if_job_exists.is_none() { | 
				
			||||
        let agent_hello = JobMeta::builder() | 
				
			||||
        .with_type(misc::JobType::Manage) | 
				
			||||
        .with_alias("agent_hello") | 
				
			||||
            .with_type(JobType::Init) | 
				
			||||
            .with_alias(job_alias) | 
				
			||||
            .build() | 
				
			||||
            .unwrap(); | 
				
			||||
    UDB::lock_db().insert_jobs(&[agent_hello]).ok(); | 
				
			||||
        UDB::lock_db().insert_jobs(&[agent_hello])?; | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
fn init_logger() { | 
				
			||||
    use simplelog::*; | 
				
			||||
    use std::fs::OpenOptions; | 
				
			||||
    let log_cfg = ConfigBuilder::new() | 
				
			||||
        .set_time_format_str("%x %X") | 
				
			||||
        .set_time_to_local(true) | 
				
			||||
        .build(); | 
				
			||||
    let logfile = OpenOptions::new() | 
				
			||||
        .append(true) | 
				
			||||
        .create(true) | 
				
			||||
        .open(LOGFILE) | 
				
			||||
        .unwrap(); | 
				
			||||
    let level = LevelFilter::Info; | 
				
			||||
    let loggers = vec![ | 
				
			||||
        WriteLogger::new(level, log_cfg.clone(), logfile) as Box<dyn SharedLogger>, | 
				
			||||
        TermLogger::new(level, log_cfg, TerminalMode::Stderr, ColorChoice::Auto), | 
				
			||||
    ]; | 
				
			||||
    CombinedLogger::init(loggers).unwrap(); | 
				
			||||
    Ok(()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn init_all() { | 
				
			||||
    init_logger(); | 
				
			||||
    init_env(); | 
				
			||||
    prefill_jobs(); | 
				
			||||
} | 
				
			||||
pub async fn serve() -> Result<(), ServerError> { | 
				
			||||
    init_logger(Some("u_server")); | 
				
			||||
    preload_jobs()?; | 
				
			||||
 | 
				
			||||
    let certs_dir = PathBuf::from("certs"); | 
				
			||||
    let env = load_env::<ServEnv>().map_err(|e| ServerError::Other(e.to_string()))?; | 
				
			||||
    let routes = init_endpoints(&env.admin_auth_token) | 
				
			||||
        .recover(handle_rejection) | 
				
			||||
        .with(custom(logger)); | 
				
			||||
 | 
				
			||||
pub async fn serve() { | 
				
			||||
    init_all(); | 
				
			||||
    let routes = make_filters(); | 
				
			||||
    warp::serve(routes.with(warp::log("warp"))) | 
				
			||||
    warp::serve(routes) | 
				
			||||
        .tls() | 
				
			||||
        .cert_path("./certs/server.crt") | 
				
			||||
        .key_path("./certs/server.key") | 
				
			||||
        .client_auth_required_path("./certs/ca.crt") | 
				
			||||
        .cert_path(certs_dir.join("server.crt")) | 
				
			||||
        .key_path(certs_dir.join("server.key")) | 
				
			||||
        .client_auth_required_path(certs_dir.join("ca.crt")) | 
				
			||||
        .run(([0, 0, 0, 0], MASTER_PORT)) | 
				
			||||
        .await; | 
				
			||||
    Ok(()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
async fn handle_rejection(rej: Rejection) -> Result<Response, Infallible> { | 
				
			||||
    let resp = if let Some(err) = rej.find::<ServerError>() { | 
				
			||||
        error!("{:x?}", err); | 
				
			||||
        RejResponse::bad_request(err.to_string()) | 
				
			||||
    } else if rej.is_not_found() { | 
				
			||||
        RejResponse::not_found("not found placeholder") | 
				
			||||
    } else { | 
				
			||||
        error!("{:?}", rej); | 
				
			||||
        RejResponse::internal() | 
				
			||||
    }; | 
				
			||||
    Ok(resp.into_response()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn logger(info: Info<'_>) { | 
				
			||||
    info!(target: "warp", | 
				
			||||
        "{raddr} {agent_uid} \"{path}\" {status}", | 
				
			||||
        raddr = info.remote_addr().unwrap_or(([0, 0, 0, 0], 0).into()), | 
				
			||||
        path = info.path(), | 
				
			||||
        agent_uid = info.user_agent() | 
				
			||||
            .map(|uid: &str| uid.splitn(3, '-') | 
				
			||||
                .take(2) | 
				
			||||
                .collect::<String>() | 
				
			||||
            ) | 
				
			||||
            .unwrap_or_else(|| "NO_AGENT".to_string()), | 
				
			||||
        status = info.status() | 
				
			||||
    ); | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn ok<T>(_: T) -> impl Reply { | 
				
			||||
    reply() | 
				
			||||
} | 
				
			||||
 | 
				
			||||
/* | 
				
			||||
#[cfg(test)] | 
				
			||||
mod tests { | 
				
			||||
    use super::*; | 
				
			||||
    #[double] | 
				
			||||
    use crate::handlers::Endpoints; | 
				
			||||
    use handlers::build_ok; | 
				
			||||
    use mockall::predicate::*; | 
				
			||||
    use test_case::test_case; | 
				
			||||
    use u_lib::messaging::{AsMsg, BaseMessage}; | 
				
			||||
    use u_lib::messaging::{AsMsg, BaseMessage, Reportable}; | 
				
			||||
    use uuid::Uuid; | 
				
			||||
    use warp::test::request; | 
				
			||||
    use warp::test; | 
				
			||||
 | 
				
			||||
    #[test_case(Some(Uuid::new_v4()))] | 
				
			||||
    #[test_case(None => panics)] | 
				
			||||
    #[rstest] | 
				
			||||
    #[case(Some(Uuid::new_v4()))] | 
				
			||||
    #[should_panic] | 
				
			||||
    #[case(None)] | 
				
			||||
    #[tokio::test] | 
				
			||||
    async fn test_get_agent_jobs_unauthorized(uid: Option<Uuid>) { | 
				
			||||
        let mock = Endpoints::get_agent_jobs_context(); | 
				
			||||
        mock.expect().with(eq(uid)).returning(|_| Ok(build_ok(""))); | 
				
			||||
        request() | 
				
			||||
    async fn test_get_agent_jobs_unauthorized(#[case] uid: Option<Uuid>) { | 
				
			||||
        let mock = Endpoints::faux(); | 
				
			||||
        when!(mock.get_agent_jobs).then_return(Ok(build_ok(""))); | 
				
			||||
        //mock.expect().with(eq(uid)).returning(|_| Ok(build_ok("")));
 | 
				
			||||
        test::request() | 
				
			||||
            .path(&format!( | 
				
			||||
                "/get_agent_jobs/{}", | 
				
			||||
                uid.map(|u| u.simple().to_string()).unwrap_or(String::new()) | 
				
			||||
            )) | 
				
			||||
            .method("GET") | 
				
			||||
            .filter(&make_filters()) | 
				
			||||
            .filter(&init_filters("")) | 
				
			||||
            .await | 
				
			||||
            .unwrap(); | 
				
			||||
        mock.checkpoint(); | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    #[tokio::test] | 
				
			||||
    async fn test_report_unauth_successful() { | 
				
			||||
        let mock = Endpoints::report_context(); | 
				
			||||
        let mock = Endpoints::report(); | 
				
			||||
        mock.expect() | 
				
			||||
            .withf(|msg: &BaseMessage<'_, Vec<ExecResult>>| msg.inner_ref()[0] == ExecResult::Dummy) | 
				
			||||
            .withf(|msg: &BaseMessage<'_, Vec<Reportable>>| msg.inner_ref()[0] == Reportable::Dummy) | 
				
			||||
            .returning(|_| Ok(build_ok(""))); | 
				
			||||
        request() | 
				
			||||
        test::request() | 
				
			||||
            .path("/report/") | 
				
			||||
            .method("POST") | 
				
			||||
            .json(&vec![ExecResult::Dummy].as_message()) | 
				
			||||
            .filter(&make_filters()) | 
				
			||||
            .json(&vec![Reportable::Dummy].as_message()) | 
				
			||||
            .filter(&init_filters("")) | 
				
			||||
            .await | 
				
			||||
            .unwrap(); | 
				
			||||
        mock.checkpoint(); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
*/ | 
				
			||||
 | 
				
			||||
@ -0,0 +1,5 @@ | 
				
			||||
FROM rust:1.64 | 
				
			||||
 | 
				
			||||
RUN rustup target add x86_64-unknown-linux-musl | 
				
			||||
RUN mkdir -p /tests && chmod 777 /tests | 
				
			||||
CMD ["sleep", "3600"] | 
				
			||||
@ -0,0 +1,17 @@ | 
				
			||||
FROM postgres:14.5 | 
				
			||||
 | 
				
			||||
ENV DEBIAN_FRONTEND=noninteractive | 
				
			||||
 | 
				
			||||
RUN apt update && apt upgrade -y | 
				
			||||
RUN apt install -y curl build-essential libpq-dev iproute2 | 
				
			||||
RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain stable --profile minimal  | 
				
			||||
ENV PATH /root/.cargo/bin:$PATH | 
				
			||||
RUN rustup target add x86_64-unknown-linux-musl | 
				
			||||
RUN cargo install diesel_cli --no-default-features --features postgres | 
				
			||||
 | 
				
			||||
RUN mkdir -p /unki | 
				
			||||
ENV LC_ALL en_US.UTF-8 | 
				
			||||
ENV LANG en_US.UTF-8 | 
				
			||||
ENV LANGUAGE en_US.UTF-8 | 
				
			||||
RUN apt install -y locales locales-all | 
				
			||||
COPY u_db_entrypoint.sh /unki/ | 
				
			||||
@ -0,0 +1,7 @@ | 
				
			||||
set -m  | 
				
			||||
 | 
				
			||||
export DATABASE_URL=postgres://${DB_USER}:${DB_PASSWORD}@127.0.0.1/${DB_NAME} | 
				
			||||
touch /unki/Cargo.toml | 
				
			||||
/usr/local/bin/docker-entrypoint.sh postgres &  | 
				
			||||
sleep 10 && diesel setup && diesel migration run | 
				
			||||
[[ $1 == "svc" ]] && fg %1 | 
				
			||||
@ -0,0 +1,3 @@ | 
				
			||||
FROM alpine:latest | 
				
			||||
 | 
				
			||||
RUN apk add iproute2 bash | 
				
			||||
@ -0,0 +1,96 @@ | 
				
			||||
FROM ubuntu:xenial | 
				
			||||
LABEL maintainer="Eirik Albrigtsen <sszynrae@gmail.com>" | 
				
			||||
 | 
				
			||||
# Required packages: | 
				
			||||
# - musl-dev, musl-tools - the musl toolchain | 
				
			||||
# - curl, g++, make, pkgconf, cmake - for fetching and building third party libs | 
				
			||||
# - ca-certificates - openssl + curl + peer verification of downloads | 
				
			||||
# - xutils-dev - for openssl makedepend | 
				
			||||
# - libssl-dev and libpq-dev - for dynamic linking during diesel_codegen build process | 
				
			||||
# - git - cargo builds in user projects | 
				
			||||
# - linux-headers-amd64 - needed for building openssl 1.1 (stretch only) | 
				
			||||
# - file - needed by rustup.sh install | 
				
			||||
# - automake autoconf libtool - support crates building C deps as part cargo build | 
				
			||||
# recently removed: | 
				
			||||
# cmake (not used), nano, zlib1g-dev | 
				
			||||
RUN apt-get update && apt-get install -y \ | 
				
			||||
  musl-dev \ | 
				
			||||
  musl-tools \ | 
				
			||||
  git \ | 
				
			||||
  file \ | 
				
			||||
  openssh-client \ | 
				
			||||
  make \ | 
				
			||||
  g++ \ | 
				
			||||
  curl \ | 
				
			||||
  pkgconf \ | 
				
			||||
  ca-certificates \ | 
				
			||||
  xutils-dev \ | 
				
			||||
  libssl-dev \ | 
				
			||||
  libpq-dev \ | 
				
			||||
  automake \ | 
				
			||||
  autoconf \ | 
				
			||||
  libtool \ | 
				
			||||
  python3 \ | 
				
			||||
  --no-install-recommends && \ | 
				
			||||
  rm -rf /var/lib/apt/lists/* | 
				
			||||
 | 
				
			||||
# Convenience list of versions and variables for compilation later on | 
				
			||||
# This helps continuing manually if anything breaks. | 
				
			||||
ENV SSL_VER="1.0.2u" \ | 
				
			||||
    CURL_VER="7.77.0" \ | 
				
			||||
    ZLIB_VER="1.2.13" \ | 
				
			||||
    PQ_VER="11.12" \ | 
				
			||||
    SQLITE_VER="3350500" \ | 
				
			||||
    CC=musl-gcc \ | 
				
			||||
    PREFIX=/musl \ | 
				
			||||
    PATH=/usr/local/bin:/root/.cargo/bin:$PATH \ | 
				
			||||
    PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \ | 
				
			||||
    LD_LIBRARY_PATH=$PREFIX | 
				
			||||
 | 
				
			||||
# Set up a prefix for musl build libraries, make the linker's job of finding them easier | 
				
			||||
# Primarily for the benefit of postgres. | 
				
			||||
# Lastly, link some linux-headers for openssl 1.1 (not used herein) | 
				
			||||
RUN mkdir $PREFIX && \ | 
				
			||||
    echo "$PREFIX/lib" >> /etc/ld-musl-x86_64.path && \ | 
				
			||||
    ln -s /usr/include/x86_64-linux-gnu/asm /usr/include/x86_64-linux-musl/asm && \ | 
				
			||||
    ln -s /usr/include/asm-generic /usr/include/x86_64-linux-musl/asm-generic && \ | 
				
			||||
    ln -s /usr/include/linux /usr/include/x86_64-linux-musl/linux | 
				
			||||
 | 
				
			||||
# Build zlib (used in openssl and pq) | 
				
			||||
RUN curl -sSL https://zlib.net/zlib-$ZLIB_VER.tar.gz | tar xz && \ | 
				
			||||
    cd zlib-$ZLIB_VER && \ | 
				
			||||
    CC="musl-gcc -fPIC -pie" LDFLAGS="-L$PREFIX/lib" CFLAGS="-I$PREFIX/include" ./configure --static --prefix=$PREFIX && \ | 
				
			||||
    make -j$(nproc) && make install && \ | 
				
			||||
    cd .. && rm -rf zlib-$ZLIB_VER | 
				
			||||
 | 
				
			||||
# Build openssl (used in curl and pq) | 
				
			||||
# Would like to use zlib here, but can't seem to get it to work properly | 
				
			||||
# TODO: fix so that it works | 
				
			||||
RUN curl -sSL https://www.openssl.org/source/old/1.0.2/openssl-$SSL_VER.tar.gz | tar xz && \ | 
				
			||||
    cd openssl-$SSL_VER && \ | 
				
			||||
    ./Configure no-zlib no-shared -fPIC --prefix=$PREFIX --openssldir=$PREFIX/ssl linux-x86_64 && \ | 
				
			||||
    env C_INCLUDE_PATH=$PREFIX/include make depend 2> /dev/null && \ | 
				
			||||
    make -j$(nproc) && make install && \ | 
				
			||||
    cd .. && rm -rf openssl-$SSL_VER | 
				
			||||
 | 
				
			||||
# Build curl (needs with-zlib and all this stuff to allow https) | 
				
			||||
# curl_LDFLAGS needed on stretch to avoid fPIC errors - though not sure from what | 
				
			||||
RUN curl -sSL https://curl.se/download/curl-$CURL_VER.tar.gz | tar xz && \ | 
				
			||||
    cd curl-$CURL_VER && \ | 
				
			||||
    CC="musl-gcc -fPIC -pie" LDFLAGS="-L$PREFIX/lib" CFLAGS="-I$PREFIX/include" ./configure \ | 
				
			||||
      --enable-shared=no --with-zlib --enable-static=ssl --enable-optimize --prefix=$PREFIX \ | 
				
			||||
      --with-ca-path=/etc/ssl/certs/ --with-ca-bundle=/etc/ssl/certs/ca-certificates.crt --without-ca-fallback \ | 
				
			||||
      --with-openssl && \ | 
				
			||||
    make -j$(nproc) curl_LDFLAGS="-all-static" && make install && \ | 
				
			||||
    cd .. && rm -rf curl-$CURL_VER | 
				
			||||
 | 
				
			||||
# Build libpq | 
				
			||||
RUN curl -sSL https://ftp.postgresql.org/pub/source/v$PQ_VER/postgresql-$PQ_VER.tar.gz | tar xz && \ | 
				
			||||
    cd postgresql-$PQ_VER && \ | 
				
			||||
    CC="musl-gcc -fPIE -pie" LDFLAGS="-L$PREFIX/lib" CFLAGS="-I$PREFIX/include" ./configure \ | 
				
			||||
    --without-readline \ | 
				
			||||
    --with-openssl \ | 
				
			||||
    --prefix=$PREFIX --host=x86_64-unknown-linux-musl && \ | 
				
			||||
    cd src/interfaces/libpq make -s -j$(nproc) all-static-lib && make -s install install-lib-static && \ | 
				
			||||
    cd ../../bin/pg_config && make -j $(nproc) && make install && \ | 
				
			||||
    cd .. && rm -rf postgresql-$PQ_VER | 
				
			||||
@ -1,66 +0,0 @@ | 
				
			||||
import subprocess | 
				
			||||
import shlex | 
				
			||||
from utils import * | 
				
			||||
from docker import docker, check_state, print_errors | 
				
			||||
 | 
				
			||||
 | 
				
			||||
class Compose: | 
				
			||||
    ALL_CONTAINERS = [ | 
				
			||||
        'u_agent_1', | 
				
			||||
        'u_agent_2', | 
				
			||||
        'u_server', | 
				
			||||
        'u_db', | 
				
			||||
        'tests_runner', | 
				
			||||
    ] | 
				
			||||
 | 
				
			||||
    def __init__(self): | 
				
			||||
        self.container_tpl = 'integration_%s_1' | 
				
			||||
        self.cmd_container = self.container_tpl % 'tests_runner' | 
				
			||||
        self.ALL_CONTAINERS = [self.container_tpl % c for c in self.ALL_CONTAINERS] | 
				
			||||
 | 
				
			||||
    def _call(self, *args): | 
				
			||||
        subprocess.check_call([ | 
				
			||||
            'docker-compose', | 
				
			||||
            '--no-ansi', | 
				
			||||
        ] + list(args) | 
				
			||||
        ) | 
				
			||||
 | 
				
			||||
    def up(self): | 
				
			||||
        log('Instanciating cluster') | 
				
			||||
        self._call('up', '-d') | 
				
			||||
        log('Ok') | 
				
			||||
 | 
				
			||||
    def down(self): | 
				
			||||
        log('Shutting down cluster') | 
				
			||||
        self._call('down') | 
				
			||||
        log('Ok') | 
				
			||||
 | 
				
			||||
    def stop(self): | 
				
			||||
        log('Stopping cluster') | 
				
			||||
        self._call('stop') | 
				
			||||
        log('Ok') | 
				
			||||
 | 
				
			||||
    def run(self, cmd): | 
				
			||||
        container = self.cmd_container | 
				
			||||
        if isinstance(cmd, str): | 
				
			||||
            cmd = shlex.split(cmd) | 
				
			||||
        log(f'Running command "{cmd}" in container {container}') | 
				
			||||
        result = docker([ | 
				
			||||
            'exec', | 
				
			||||
            '-ti', | 
				
			||||
            container | 
				
			||||
        ] + cmd) | 
				
			||||
        log('Ok') | 
				
			||||
        return result | 
				
			||||
 | 
				
			||||
    def is_alive(self): | 
				
			||||
        log('Check if all containers are alive') | 
				
			||||
 | 
				
			||||
        errors = check_state(self.ALL_CONTAINERS) | 
				
			||||
        log('Check done') | 
				
			||||
 | 
				
			||||
        if errors: | 
				
			||||
            print_errors(errors) | 
				
			||||
            raise TestsError('Error during `is_alive` check') | 
				
			||||
        else: | 
				
			||||
            log('All containers are alive') | 
				
			||||
@ -1,4 +0,0 @@ | 
				
			||||
FROM rust:1.53 | 
				
			||||
 | 
				
			||||
RUN rustup target add x86_64-unknown-linux-musl | 
				
			||||
CMD ["sleep", "3600"] | 
				
			||||
@ -1,3 +0,0 @@ | 
				
			||||
FROM postgres:13.3 | 
				
			||||
 | 
				
			||||
RUN apt update && apt -y upgrade && apt install -y iproute2 | 
				
			||||
@ -1,3 +0,0 @@ | 
				
			||||
FROM rust:1.53 | 
				
			||||
 | 
				
			||||
RUN cargo install diesel_cli --no-default-features --features postgres | 
				
			||||
@ -1,3 +1,6 @@ | 
				
			||||
#!/bin/bash | 
				
			||||
set -e | 
				
			||||
export DOCKER_UID=$(id -u) | 
				
			||||
export DOCKER_GID=$(id -g) | 
				
			||||
[[ "$@" =~ "--release" ]] && export PROFILE=release || export PROFILE=debug | 
				
			||||
python integration_tests.py $@ | 
				
			||||
 | 
				
			||||
@ -1 +0,0 @@ | 
				
			||||
 | 
				
			||||
@ -0,0 +1,35 @@ | 
				
			||||
use crate::helpers::ENV; | 
				
			||||
use u_lib::{api::ClientHandler, messaging::Reportable, models::*}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
 | 
				
			||||
pub struct RegisteredAgent { | 
				
			||||
    pub uid: Uuid, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
impl RegisteredAgent { | 
				
			||||
    pub async fn unregister(self) { | 
				
			||||
        let cli = ClientHandler::new(&ENV.u_server, None); | 
				
			||||
        cli.del(self.uid).await.unwrap(); | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[fixture] | 
				
			||||
pub async fn register_agent() -> RegisteredAgent { | 
				
			||||
    let cli = ClientHandler::new(&ENV.u_server, None); | 
				
			||||
    let agent_uid = Uuid::new_v4(); | 
				
			||||
    println!("registering agent {agent_uid}"); | 
				
			||||
    let resp = cli | 
				
			||||
        .get_personal_jobs(agent_uid) | 
				
			||||
        .await | 
				
			||||
        .unwrap() | 
				
			||||
        .pop() | 
				
			||||
        .unwrap(); | 
				
			||||
    let job_id = resp.job_id; | 
				
			||||
    let job = cli.get_jobs(Some(job_id)).await.unwrap().pop().unwrap(); | 
				
			||||
    assert_eq!(job.alias, Some("agent_hello".to_string())); | 
				
			||||
    let mut agent_data = AssignedJob::from(&job); | 
				
			||||
    agent_data.agent_id = agent_uid; | 
				
			||||
    agent_data.set_result(&Agent::with_id(agent_uid)); | 
				
			||||
    cli.report(Reportable::Assigned(agent_data)).await.unwrap(); | 
				
			||||
    RegisteredAgent { uid: agent_uid } | 
				
			||||
} | 
				
			||||
@ -0,0 +1 @@ | 
				
			||||
pub mod agent; | 
				
			||||
@ -1,48 +0,0 @@ | 
				
			||||
use reqwest::{Client, RequestBuilder, Url}; | 
				
			||||
use serde::Serialize; | 
				
			||||
use serde_json::{from_str, json, Value}; | 
				
			||||
 | 
				
			||||
const SERVER: &str = "u_server"; | 
				
			||||
const PORT: &str = "63714"; | 
				
			||||
 | 
				
			||||
pub struct AgentClient { | 
				
			||||
    client: Client, | 
				
			||||
    base_url: Url, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
impl AgentClient { | 
				
			||||
    pub fn new() -> Self { | 
				
			||||
        Self { | 
				
			||||
            client: Client::new(), | 
				
			||||
            base_url: Url::parse(&format!("http://{}:{}", SERVER, PORT)).unwrap(), | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    async fn process_request(&self, req: RequestBuilder, resp_needed: bool) -> Value { | 
				
			||||
        let resp = req.send().await.unwrap(); | 
				
			||||
        if let Err(e) = resp.error_for_status_ref() { | 
				
			||||
            panic!( | 
				
			||||
                "Server responded with code {}\nError: {}", | 
				
			||||
                e.status() | 
				
			||||
                    .map(|s| s.to_string()) | 
				
			||||
                    .unwrap_or(String::from("<none>")), | 
				
			||||
                e.to_string() | 
				
			||||
            ); | 
				
			||||
        } | 
				
			||||
        if !resp_needed { | 
				
			||||
            return json!([]); | 
				
			||||
        } | 
				
			||||
        let resp: Value = from_str(&resp.text().await.unwrap()).unwrap(); | 
				
			||||
        resp.get("inner").unwrap().get(0).unwrap().clone() | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn get<S: AsRef<str>>(&self, url: S) -> Value { | 
				
			||||
        let req = self.client.get(self.base_url.join(url.as_ref()).unwrap()); | 
				
			||||
        self.process_request(req, true).await | 
				
			||||
    } | 
				
			||||
 | 
				
			||||
    pub async fn post<S: AsRef<str>, B: Serialize>(&self, url: S, body: &B) -> Value { | 
				
			||||
        let req = self.client.post(self.base_url.join(url.as_ref()).unwrap()); | 
				
			||||
        self.process_request(req.json(body), false).await | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -1,3 +1,8 @@ | 
				
			||||
pub mod panel; | 
				
			||||
 | 
				
			||||
pub use panel::Panel; | 
				
			||||
 | 
				
			||||
use once_cell::sync::Lazy; | 
				
			||||
use u_lib::utils::{env::DefaultEnv, load_env_default}; | 
				
			||||
 | 
				
			||||
pub static ENV: Lazy<DefaultEnv> = Lazy::new(|| load_env_default().unwrap()); | 
				
			||||
 | 
				
			||||
@ -0,0 +1,51 @@ | 
				
			||||
use crate::fixtures::agent::*; | 
				
			||||
use crate::helpers::Panel; | 
				
			||||
 | 
				
			||||
use rstest::rstest; | 
				
			||||
use serde_json::{json, to_string}; | 
				
			||||
use std::error::Error; | 
				
			||||
use std::time::Duration; | 
				
			||||
use tokio::time::sleep; | 
				
			||||
use u_lib::models::*; | 
				
			||||
use uuid::Uuid; | 
				
			||||
 | 
				
			||||
type TestResult<R = ()> = Result<R, Box<dyn Error>>; | 
				
			||||
 | 
				
			||||
#[rstest] | 
				
			||||
#[tokio::test] | 
				
			||||
async fn test_registration(#[future] register_agent: RegisteredAgent) -> TestResult { | 
				
			||||
    let agent = register_agent.await; | 
				
			||||
    let agents: Vec<Agent> = Panel::check_output("agents read"); | 
				
			||||
    let found = agents.iter().find(|v| v.id == agent.uid); | 
				
			||||
    assert!(found.is_some()); | 
				
			||||
    Panel::check_status(format!("agents delete {}", agent.uid)); | 
				
			||||
    Ok(()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[tokio::test] | 
				
			||||
async fn test_setup_tasks() -> TestResult { | 
				
			||||
    let agents: Vec<Agent> = Panel::check_output("agents read"); | 
				
			||||
    let agent_uid = match agents.get(0) { | 
				
			||||
        Some(a) => a.id, | 
				
			||||
        None => panic!("Some independent agents should present"), | 
				
			||||
    }; | 
				
			||||
    let job_alias = "passwd_contents"; | 
				
			||||
    let job = json!( | 
				
			||||
        {"alias": job_alias, "payload": b"cat /etc/passwd", "argv": "/bin/bash {}" } | 
				
			||||
    ); | 
				
			||||
    let cmd = format!("jobs create '{}'", to_string(&job).unwrap()); | 
				
			||||
    Panel::check_status(cmd); | 
				
			||||
    let cmd = format!("map create {} {}", agent_uid, job_alias); | 
				
			||||
    let assigned_uids: Vec<Uuid> = Panel::check_output(cmd); | 
				
			||||
    for _ in 0..3 { | 
				
			||||
        let result: Vec<AssignedJob> = | 
				
			||||
            Panel::check_output(format!("map read {}", assigned_uids[0])); | 
				
			||||
        if result[0].state == JobState::Finished { | 
				
			||||
            return Ok(()); | 
				
			||||
        } else { | 
				
			||||
            sleep(Duration::from_secs(5)).await; | 
				
			||||
            eprintln!("waiting for task"); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
    panic!("Job didn't appear in the job map"); | 
				
			||||
} | 
				
			||||
@ -0,0 +1,22 @@ | 
				
			||||
use crate::helpers::ENV; | 
				
			||||
use u_lib::config::MASTER_PORT; | 
				
			||||
 | 
				
			||||
#[tokio::test] | 
				
			||||
async fn test_non_auth_connection_dropped() { | 
				
			||||
    let client = reqwest::ClientBuilder::new() | 
				
			||||
        .danger_accept_invalid_certs(true) | 
				
			||||
        .build() | 
				
			||||
        .unwrap(); | 
				
			||||
    match client | 
				
			||||
        .get(format!("https://{}:{}", &ENV.u_server, MASTER_PORT)) | 
				
			||||
        .send() | 
				
			||||
        .await | 
				
			||||
    { | 
				
			||||
        Err(e) => { | 
				
			||||
            let err = e.to_string(); | 
				
			||||
            println!("captured err: {err}"); | 
				
			||||
            assert!(err.contains("certificate required")); | 
				
			||||
        } | 
				
			||||
        _ => panic!("no error occured on foreign client connection"), | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
@ -0,0 +1,2 @@ | 
				
			||||
mod behaviour; | 
				
			||||
mod connection; | 
				
			||||
@ -0,0 +1,6 @@ | 
				
			||||
mod fixtures; | 
				
			||||
mod helpers; | 
				
			||||
mod integration; | 
				
			||||
 | 
				
			||||
#[macro_use] | 
				
			||||
extern crate rstest; | 
				
			||||
@ -1,67 +0,0 @@ | 
				
			||||
mod helpers; | 
				
			||||
 | 
				
			||||
use helpers::Panel; | 
				
			||||
 | 
				
			||||
use std::error::Error; | 
				
			||||
use std::thread::sleep; | 
				
			||||
use std::time::Duration; | 
				
			||||
use u_lib::{api::ClientHandler, models::*}; | 
				
			||||
use uuid::Uuid; | 
				
			||||
 | 
				
			||||
type TestResult<R = ()> = Result<R, Box<dyn Error>>; | 
				
			||||
 | 
				
			||||
async fn register_agent() -> Uuid { | 
				
			||||
    let cli = ClientHandler::new(None); | 
				
			||||
    let agent_uid = Uuid::new_v4(); | 
				
			||||
    let resp = cli | 
				
			||||
        .get_personal_jobs(Some(agent_uid)) | 
				
			||||
        .await | 
				
			||||
        .unwrap() | 
				
			||||
        .pop() | 
				
			||||
        .unwrap(); | 
				
			||||
    let job_id = resp.job_id; | 
				
			||||
    let resp = cli.get_jobs(Some(job_id)).await.unwrap().pop().unwrap(); | 
				
			||||
    assert_eq!(resp.alias, Some("agent_hello".to_string())); | 
				
			||||
    let agent_data = Agent { | 
				
			||||
        id: agent_uid, | 
				
			||||
        ..Default::default() | 
				
			||||
    }; | 
				
			||||
    cli.report(&vec![ExecResult::Agent(agent_data)]) | 
				
			||||
        .await | 
				
			||||
        .unwrap(); | 
				
			||||
    agent_uid | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[tokio::test] | 
				
			||||
async fn test_registration() -> TestResult { | 
				
			||||
    let agent_uid = register_agent().await; | 
				
			||||
    let agents: Vec<Agent> = Panel::check_output("agents list"); | 
				
			||||
    let found = agents.iter().find(|v| v.id == agent_uid); | 
				
			||||
    assert!(found.is_some()); | 
				
			||||
    //teardown
 | 
				
			||||
    Panel::check_status::<i32>(&format!("agents delete {}", agent_uid)); | 
				
			||||
    Ok(()) | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[tokio::test] | 
				
			||||
async fn test_setup_tasks() -> TestResult { | 
				
			||||
    //some independent agents should present
 | 
				
			||||
    let agents: Vec<Agent> = Panel::check_output("agents list"); | 
				
			||||
    let agent_uid = agents[0].id; | 
				
			||||
    let job_alias = "passwd_contents"; | 
				
			||||
    let cmd = format!("jobs add --alias {} 'cat /etc/passwd'", job_alias); | 
				
			||||
    Panel::check_status::<Empty>(&cmd); | 
				
			||||
    let cmd = format!("jobmap add {} {}", agent_uid, job_alias); | 
				
			||||
    let assigned_uids: Vec<Uuid> = Panel::check_output(cmd); | 
				
			||||
    for _ in 0..3 { | 
				
			||||
        let result: Vec<AssignedJob> = | 
				
			||||
            Panel::check_output(format!("jobmap list {}", assigned_uids[0])); | 
				
			||||
        if result[0].state == JobState::Finished { | 
				
			||||
            return Ok(()); | 
				
			||||
        } else { | 
				
			||||
            sleep(Duration::from_secs(5)); | 
				
			||||
            eprintln!("waiting for task"); | 
				
			||||
        } | 
				
			||||
    } | 
				
			||||
    panic!() | 
				
			||||
} | 
				
			||||
@ -1,16 +0,0 @@ | 
				
			||||
[package] | 
				
			||||
name = "u_api_proc_macro" | 
				
			||||
version = "0.1.0" | 
				
			||||
authors = ["plazmoid <kronos44@mail.ru>"] | 
				
			||||
edition = "2018" | 
				
			||||
 | 
				
			||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | 
				
			||||
 | 
				
			||||
[lib] | 
				
			||||
proc-macro = true | 
				
			||||
 | 
				
			||||
[dependencies] | 
				
			||||
syn = { version = "1.0", features = ["full", "extra-traits"] } | 
				
			||||
quote = "1.0" | 
				
			||||
strum = { version = "0.20", features = ["derive"] } | 
				
			||||
proc-macro2 = "1.0" | 
				
			||||
@ -1,179 +0,0 @@ | 
				
			||||
use proc_macro::TokenStream; | 
				
			||||
use proc_macro2::{Ident, TokenStream as TokenStream2}; | 
				
			||||
use quote::quote; | 
				
			||||
use std::{collections::HashMap, str::FromStr}; | 
				
			||||
use strum::EnumString; | 
				
			||||
use syn::{ | 
				
			||||
    parse_macro_input, punctuated::Punctuated, AttributeArgs, FnArg, ItemFn, Lit, NestedMeta, | 
				
			||||
    ReturnType, Signature, Token, Type, | 
				
			||||
}; | 
				
			||||
 | 
				
			||||
#[derive(EnumString, Debug)] | 
				
			||||
enum ReqMethod { | 
				
			||||
    GET, | 
				
			||||
    POST, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(Debug)] | 
				
			||||
struct Endpoint { | 
				
			||||
    method: ReqMethod, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[derive(Debug)] | 
				
			||||
struct FnArgs { | 
				
			||||
    url_param: Option<Type>, | 
				
			||||
    payload: Option<Type>, | 
				
			||||
} | 
				
			||||
 | 
				
			||||
#[proc_macro_attribute] | 
				
			||||
pub fn api_route(args: TokenStream, item: TokenStream) -> TokenStream { | 
				
			||||
    let args: AttributeArgs = parse_macro_input!(args); | 
				
			||||
    let input: ItemFn = parse_macro_input!(item); | 
				
			||||
    let Signature { | 
				
			||||
        ident, | 
				
			||||
        inputs, | 
				
			||||
        generics, | 
				
			||||
        output, | 
				
			||||
        .. | 
				
			||||
    } = input.sig; | 
				
			||||
    let (impl_generics, _, _) = generics.split_for_impl(); | 
				
			||||
    let FnArgs { url_param, payload } = parse_fn_args(inputs); | 
				
			||||
    let Endpoint { method } = parse_attr_args(args); | 
				
			||||
    let url_path = build_url_path(&ident, &url_param); | 
				
			||||
    let return_ty = match output { | 
				
			||||
        ReturnType::Type(_, ty) => quote!(#ty), | 
				
			||||
        ReturnType::Default => quote!(()), | 
				
			||||
    }; | 
				
			||||
    let request = match method { | 
				
			||||
        ReqMethod::GET => build_get(url_path), | 
				
			||||
        ReqMethod::POST => build_post(url_path, &payload), | 
				
			||||
    }; | 
				
			||||
    let url_param = match url_param { | 
				
			||||
        Some(p) => quote!(, param: #p), | 
				
			||||
        None => TokenStream2::new(), | 
				
			||||
    }; | 
				
			||||
    let payload = match payload { | 
				
			||||
        Some(p) => quote!(, payload: #p), | 
				
			||||
        None => TokenStream2::new(), | 
				
			||||
    }; | 
				
			||||
    let q = quote! { | 
				
			||||
        pub async fn #ident #impl_generics( | 
				
			||||
            &self #url_param #payload | 
				
			||||
        ) -> UResult<#return_ty> { | 
				
			||||
            let request = { | 
				
			||||
                #request | 
				
			||||
            }; | 
				
			||||
            let response = request.send().await?; | 
				
			||||
            let content_len = response.content_length(); | 
				
			||||
            let is_success = match response.error_for_status_ref() { | 
				
			||||
                Ok(_) => Ok(()), | 
				
			||||
                Err(e) => Err(UError::from(e)) | 
				
			||||
            }; | 
				
			||||
            match is_success { | 
				
			||||
                Ok(_) => response.json::<BaseMessage<#return_ty>>() | 
				
			||||
                    .await | 
				
			||||
                    .map(|msg| msg.into_inner()) | 
				
			||||
                    .or_else(|e| { | 
				
			||||
                        match content_len { | 
				
			||||
                            Some(0) => Ok(Default::default()), | 
				
			||||
                            _ => Err(UError::from(e)) | 
				
			||||
                        } | 
				
			||||
                    }), | 
				
			||||
                Err(UError::NetError(err_src, _)) => Err( | 
				
			||||
                    UError::NetError( | 
				
			||||
                        err_src, | 
				
			||||
                        response.text().await.unwrap() | 
				
			||||
                    ) | 
				
			||||
                ), | 
				
			||||
                _ => unreachable!() | 
				
			||||
            } | 
				
			||||
        } | 
				
			||||
    }; | 
				
			||||
    //eprintln!("#!#! RESULT:\n{}", q);
 | 
				
			||||
    q.into() | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn parse_fn_args(raw: Punctuated<FnArg, Token![,]>) -> FnArgs { | 
				
			||||
    let mut arg: HashMap<String, Type> = raw | 
				
			||||
        .into_iter() | 
				
			||||
        .filter_map(|arg| { | 
				
			||||
            if let FnArg::Typed(argt) = arg { | 
				
			||||
                let mut arg_name = String::new(); | 
				
			||||
                // did you think I won't overplay you? won't destroy?
 | 
				
			||||
                |arg_ident| -> TokenStream { | 
				
			||||
                    let q: TokenStream = quote!(#arg_ident).into(); | 
				
			||||
                    arg_name = parse_macro_input!(q as Ident).to_string(); | 
				
			||||
                    TokenStream::new() | 
				
			||||
                }(argt.pat); | 
				
			||||
                if &arg_name != "url_param" && &arg_name != "payload" { | 
				
			||||
                    panic!("Wrong arg name: {}", &arg_name) | 
				
			||||
                } | 
				
			||||
                let arg_type = *argt.ty.clone(); | 
				
			||||
                Some((arg_name, arg_type)) | 
				
			||||
            } else { | 
				
			||||
                None | 
				
			||||
            } | 
				
			||||
        }) | 
				
			||||
        .collect(); | 
				
			||||
    FnArgs { | 
				
			||||
        url_param: arg.remove("url_param"), | 
				
			||||
        payload: arg.remove("payload"), | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn build_get(url: TokenStream2) -> TokenStream2 { | 
				
			||||
    quote! { | 
				
			||||
        let request = self.build_get(#url); | 
				
			||||
        request | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn build_post(url: TokenStream2, payload: &Option<Type>) -> TokenStream2 { | 
				
			||||
    let pld = match payload { | 
				
			||||
        Some(_) => quote! { | 
				
			||||
            .json(&payload.as_message()) | 
				
			||||
        }, | 
				
			||||
        None => TokenStream2::new(), | 
				
			||||
    }; | 
				
			||||
    quote! { | 
				
			||||
        let request = self.build_post(#url); | 
				
			||||
        request #pld | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn build_url_path(path: &Ident, url_param: &Option<Type>) -> TokenStream2 { | 
				
			||||
    let url_param = match url_param { | 
				
			||||
        Some(_) => quote! { | 
				
			||||
            + &opt_to_string(param) | 
				
			||||
        }, | 
				
			||||
        None => TokenStream2::new(), | 
				
			||||
    }; | 
				
			||||
    quote! { | 
				
			||||
        &format!( | 
				
			||||
            "{}/{}", | 
				
			||||
            stringify!(#path), | 
				
			||||
            String::new() #url_param | 
				
			||||
        ) | 
				
			||||
    } | 
				
			||||
} | 
				
			||||
 | 
				
			||||
fn parse_attr_args(args: AttributeArgs) -> Endpoint { | 
				
			||||
    let mut args = args.into_iter(); | 
				
			||||
    let method = match args.next() { | 
				
			||||
        Some(method) => match method { | 
				
			||||
            NestedMeta::Lit(l) => { | 
				
			||||
                if let Lit::Str(s) = l { | 
				
			||||
                    match ReqMethod::from_str(&s.value()) { | 
				
			||||
                        Ok(v) => v, | 
				
			||||
                        Err(_) => panic!("Unknown method"), | 
				
			||||
                    } | 
				
			||||
                } else { | 
				
			||||
                    panic!("Method must be a str") | 
				
			||||
                } | 
				
			||||
            } | 
				
			||||
            _ => panic!("Method must be on the first place"), | 
				
			||||
        }, | 
				
			||||
        None => panic!("Method required"), | 
				
			||||
    }; | 
				
			||||
    Endpoint { method } | 
				
			||||
} | 
				
			||||
Some files were not shown because too many files have changed in this diff Show More
					Loading…
					
					
				
		Reference in new issue