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/ | 
					target/ | 
				
			||||||
**/*.rs.bk | 
					 | 
				
			||||||
.idea/ | 
					.idea/ | 
				
			||||||
data/ | 
					data/ | 
				
			||||||
 | 
					certs/ | 
				
			||||||
 | 
					static/ | 
				
			||||||
 | 
					.vscode/ | 
				
			||||||
 | 
					release/ | 
				
			||||||
 | 
					**/node_modules/ | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**/*.rs.bk | 
				
			||||||
**/*.pyc | 
					**/*.pyc | 
				
			||||||
certs/* | 
					 | 
				
			||||||
*.log | 
					*.log | 
				
			||||||
echoer | 
					echoer | 
				
			||||||
.env.private | 
					.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:
 | 
					// TODO:
 | 
				
			||||||
// поддержка питона
 | 
					// поддержка питона
 | 
				
			||||||
// резолв адреса управляющего сервера через DoT
 | 
					// резолв адреса управляющего сервера через DoT
 | 
				
			||||||
// кроссплатформенность (реализовать интерфейс для винды и никсов)
 | 
					 | 
				
			||||||
// проверка обнов
 | 
					 | 
				
			||||||
// самоуничтожение
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[macro_use] | 
					#[macro_use] | 
				
			||||||
extern crate log; | 
					extern crate log; | 
				
			||||||
extern crate env_logger; | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use std::env; | 
					//use daemonize::Daemonize;
 | 
				
			||||||
 | 
					use std::sync::Arc; | 
				
			||||||
use tokio::time::{sleep, Duration}; | 
					use tokio::time::{sleep, Duration}; | 
				
			||||||
use u_lib::{ | 
					use u_lib::{ | 
				
			||||||
    api::ClientHandler, | 
					    api::ClientHandler, cache::JobCache, config::get_self_uid, errors::ErrChan, | 
				
			||||||
    builder::JobBuilder, | 
					    executor::pop_completed, logging::init_logger, messaging::Reportable, models::AssignedJobById, | 
				
			||||||
    cache::JobCache, | 
					    runner::JobRunner, utils::load_env_default, | 
				
			||||||
    executor::pop_completed, | 
					 | 
				
			||||||
    models::{AssignedJob, ExecResult}, | 
					 | 
				
			||||||
    UID, | 
					 | 
				
			||||||
    //daemonize
 | 
					 | 
				
			||||||
}; | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
#[macro_export] | 
					const ITERATION_LATENCY: u64 = 5; | 
				
			||||||
macro_rules! retry_until_ok { | 
					 | 
				
			||||||
    ( $body:expr ) => { | 
					 | 
				
			||||||
        loop { | 
					 | 
				
			||||||
            match $body { | 
					 | 
				
			||||||
                Ok(r) => break r, | 
					 | 
				
			||||||
                Err(e) => error!("{:?}", e), | 
					 | 
				
			||||||
            }; | 
					 | 
				
			||||||
            sleep(Duration::from_secs(5)).await; | 
					 | 
				
			||||||
        } | 
					 | 
				
			||||||
    }; | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn process_request(job_requests: Vec<AssignedJob>, client: &ClientHandler) { | 
					pub async fn process_request(jobs: Vec<AssignedJobById>, client: &ClientHandler) { | 
				
			||||||
    if job_requests.len() > 0 { | 
					    if !jobs.is_empty() { | 
				
			||||||
        for jr in &job_requests { | 
					        for jr in &jobs { | 
				
			||||||
            if !JobCache::contains(&jr.job_id) { | 
					            if !JobCache::contains(jr.job_id) { | 
				
			||||||
                info!("Fetching job: {}", &jr.job_id); | 
					                info!("Fetching job: {}", &jr.job_id); | 
				
			||||||
                let fetched_job = retry_until_ok!(client.get_jobs(Some(jr.job_id)).await) | 
					                let fetched_job = loop { | 
				
			||||||
                    .pop() | 
					                    match client.get_jobs(Some(jr.job_id)).await { | 
				
			||||||
                    .unwrap(); | 
					                        Ok(mut result) => break result.pop().unwrap(), | 
				
			||||||
 | 
					                        Err(err) => { | 
				
			||||||
 | 
					                            debug!("{:?} \nretrying...", err); | 
				
			||||||
 | 
					                            sleep(Duration::from_secs(ITERATION_LATENCY)).await; | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                }; | 
				
			||||||
                JobCache::insert(fetched_job); | 
					                JobCache::insert(fetched_job); | 
				
			||||||
            } | 
					            } | 
				
			||||||
        } | 
					        } | 
				
			||||||
        info!( | 
					        info!( | 
				
			||||||
            "Scheduling jobs: {}", | 
					            "Scheduling jobs: {}", | 
				
			||||||
            job_requests | 
					            jobs.iter() | 
				
			||||||
                .iter() | 
					 | 
				
			||||||
                .map(|j| j.job_id.to_string()) | 
					                .map(|j| j.job_id.to_string()) | 
				
			||||||
                .collect::<Vec<String>>() | 
					                .collect::<Vec<String>>() | 
				
			||||||
                .join(", ") | 
					                .join(", ") | 
				
			||||||
        ); | 
					        ); | 
				
			||||||
        let mut builder = JobBuilder::from_request(job_requests); | 
					        let mut runner = JobRunner::from_jobs(jobs); | 
				
			||||||
        let errors = builder.pop_errors(); | 
					        let errors = runner.pop_errors(); | 
				
			||||||
        if errors.len() > 0 { | 
					        if !errors.is_empty() { | 
				
			||||||
            error!( | 
					            for e in errors { | 
				
			||||||
                "Some errors encountered: \n{}", | 
					                ErrChan::send(e, "ebld").await; | 
				
			||||||
                errors | 
					            } | 
				
			||||||
                    .iter() | 
					        } | 
				
			||||||
                    .map(|j| j.to_string()) | 
					        runner.unwrap_one().spawn().await; | 
				
			||||||
                    .collect::<Vec<String>>() | 
					    } | 
				
			||||||
                    .join("\n") | 
					} | 
				
			||||||
            ); | 
					
 | 
				
			||||||
 | 
					async fn error_reporting(client: Arc<ClientHandler>) -> ! { | 
				
			||||||
 | 
					    loop { | 
				
			||||||
 | 
					        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; | 
				
			||||||
 | 
					                        } | 
				
			||||||
 | 
					                    } | 
				
			||||||
 | 
					                } | 
				
			||||||
 | 
					            } | 
				
			||||||
 | 
					            None => sleep(Duration::from_secs(3)).await, | 
				
			||||||
        } | 
					        } | 
				
			||||||
        builder.unwrap_one().spawn().await; | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn run_forever() { | 
					async fn agent_loop(client: Arc<ClientHandler>) -> ! { | 
				
			||||||
    //daemonize();
 | 
					 | 
				
			||||||
    env_logger::init(); | 
					 | 
				
			||||||
    let arg_ip = env::args().nth(1); | 
					 | 
				
			||||||
    let instance = ClientHandler::new(arg_ip.as_deref()); | 
					 | 
				
			||||||
    info!("Connecting to the server"); | 
					 | 
				
			||||||
    loop { | 
					    loop { | 
				
			||||||
        let job_requests: Vec<AssignedJob> = | 
					        match client.get_personal_jobs(get_self_uid()).await { | 
				
			||||||
            retry_until_ok!(instance.get_personal_jobs(Some(*UID)).await).into_builtin_vec(); | 
					            Ok(jobs) => { | 
				
			||||||
        process_request(job_requests, &instance).await; | 
					                process_request(jobs, &client).await; | 
				
			||||||
        let result: Vec<ExecResult> = pop_completed().await.into_iter().collect(); | 
					            } | 
				
			||||||
        if result.len() > 0 { | 
					            Err(err) => ErrChan::send(err, "processing").await, | 
				
			||||||
            retry_until_ok!(instance.report(&result).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(5)).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; | 
					mod argparse; | 
				
			||||||
use std::fmt; | 
					mod server; | 
				
			||||||
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)] | 
					#[macro_use] | 
				
			||||||
enum JobALD { | 
					extern crate tracing; | 
				
			||||||
    Add { | 
					 | 
				
			||||||
        #[structopt(long, parse(try_from_str = parse_uuid))] | 
					 | 
				
			||||||
        agent: Option<Uuid>, | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #[structopt(long)] | 
					use anyhow::Result as AnyResult; | 
				
			||||||
        alias: String, | 
					use argparse::{process_cmd, Args}; | 
				
			||||||
 | 
					use serde::Deserialize; | 
				
			||||||
        #[structopt(subcommand)] | 
					use structopt::StructOpt; | 
				
			||||||
        cmd: JobCmd, | 
					use u_lib::api::ClientHandler; | 
				
			||||||
    }, | 
					use u_lib::logging::init_logger; | 
				
			||||||
    #[structopt(flatten)] | 
					use u_lib::utils::{env::default_host, load_env}; | 
				
			||||||
    LD(LD), | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(StructOpt, Debug)] | 
					 | 
				
			||||||
enum JobCmd { | 
					 | 
				
			||||||
    #[structopt(external_subcommand)] | 
					 | 
				
			||||||
    Cmd(Vec<String>), | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(StructOpt, Debug)] | 
					 | 
				
			||||||
enum JobMapALD { | 
					 | 
				
			||||||
    Add { | 
					 | 
				
			||||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
					 | 
				
			||||||
        agent_uid: Uuid, | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        job_idents: Vec<String>, | 
					 | 
				
			||||||
    }, | 
					 | 
				
			||||||
    List { | 
					 | 
				
			||||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
					 | 
				
			||||||
        uid: Option<Uuid>, | 
					 | 
				
			||||||
    }, | 
					 | 
				
			||||||
    Delete { | 
					 | 
				
			||||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
					 | 
				
			||||||
        uid: Uuid, | 
					 | 
				
			||||||
    }, | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#[derive(StructOpt, Debug)] | 
					 | 
				
			||||||
enum LD { | 
					 | 
				
			||||||
    List { | 
					 | 
				
			||||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
					 | 
				
			||||||
        uid: Option<Uuid>, | 
					 | 
				
			||||||
    }, | 
					 | 
				
			||||||
    Delete { | 
					 | 
				
			||||||
        #[structopt(parse(try_from_str = parse_uuid))] | 
					 | 
				
			||||||
        uid: Uuid, | 
					 | 
				
			||||||
    }, | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
fn parse_uuid(src: &str) -> Result<Uuid, String> { | 
					#[derive(Deserialize)] | 
				
			||||||
    Uuid::parse_str(src).map_err(|e| e.to_string()) | 
					struct AccessEnv { | 
				
			||||||
 | 
					    admin_auth_token: String, | 
				
			||||||
 | 
					    #[serde(default = "default_host")] | 
				
			||||||
 | 
					    u_server: String, | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn process_cmd(args: Args) { | 
					#[actix_web::main] | 
				
			||||||
    struct Printer { | 
					async fn main() -> AnyResult<()> { | 
				
			||||||
        json: bool, | 
					    let env = load_env::<AccessEnv>()?; | 
				
			||||||
    } | 
					    let client = ClientHandler::new(&env.u_server, Some(env.admin_auth_token)); | 
				
			||||||
 | 
					    let args = Args::from_args(); | 
				
			||||||
 | 
					
 | 
				
			||||||
    impl Printer { | 
					    init_logger(None::<&str>); | 
				
			||||||
        pub fn print<Msg: AsMsg + fmt::Display>(&self, data: UResult<Msg>) { | 
					 | 
				
			||||||
            if self.json { | 
					 | 
				
			||||||
                let data = match data { | 
					 | 
				
			||||||
                    Ok(r) => DataResult::Ok(r), | 
					 | 
				
			||||||
                    Err(e) => DataResult::Err(e), | 
					 | 
				
			||||||
                }; | 
					 | 
				
			||||||
                println!("{}", serde_json::to_string_pretty(&data).unwrap()); | 
					 | 
				
			||||||
            } else { | 
					 | 
				
			||||||
                match data { | 
					 | 
				
			||||||
                    Ok(r) => println!("{}", r), | 
					 | 
				
			||||||
                    Err(e) => eprintln!("Error: {}", e), | 
					 | 
				
			||||||
                } | 
					 | 
				
			||||||
            } | 
					 | 
				
			||||||
        } | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let token = env::var("ADMIN_AUTH_TOKEN").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), | 
					 | 
				
			||||||
        }, | 
					 | 
				
			||||||
    } | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main] | 
					    let result = process_cmd(client, args).await.to_string(); | 
				
			||||||
async fn main() { | 
					    println!("{result}"); | 
				
			||||||
    init_env(); | 
					    Ok(()) | 
				
			||||||
    let args: Args = Args::from_args(); | 
					 | 
				
			||||||
    process_cmd(args).await; | 
					 | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
				
			|||||||
@ -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 crate::db::UDB; | 
				
			||||||
use diesel::SaveChangesDsl; | 
					use crate::error::Error; | 
				
			||||||
use hyper::Body; | 
					 | 
				
			||||||
use serde::Serialize; | 
					 | 
				
			||||||
use u_lib::{ | 
					use u_lib::{ | 
				
			||||||
    messaging::{AsMsg, BaseMessage}, | 
					    messaging::{AsMsg, BaseMessage, Reportable}, | 
				
			||||||
    models::{Agent, AgentState, AssignedJob, ExecResult, JobMeta, JobState}, | 
					    models::*, | 
				
			||||||
    ULocalError, | 
					    utils::OneOrVec, | 
				
			||||||
}; | 
					}; | 
				
			||||||
use uuid::Uuid; | 
					use uuid::Uuid; | 
				
			||||||
use warp::{ | 
					use warp::Rejection; | 
				
			||||||
    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()) | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn build_message<M: AsMsg + Serialize>(m: M) -> Response<Body> { | 
					type EndpResult<T> = Result<T, Rejection>; | 
				
			||||||
    warp::reply::json(&m.as_message()).into_response() | 
					 | 
				
			||||||
} | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub struct Endpoints; | 
					pub struct Endpoints; | 
				
			||||||
 | 
					
 | 
				
			||||||
#[cfg_attr(test, automock)] | 
					 | 
				
			||||||
impl Endpoints { | 
					impl Endpoints { | 
				
			||||||
    pub async fn add_agent(msg: Agent) -> Result<Response<Body>, Rejection> { | 
					    pub async fn add_agent(msg: Agent) -> EndpResult<()> { | 
				
			||||||
        info!("hnd: add_agent"); | 
					        UDB::lock_db().insert_agent(&msg).map_err(From::from) | 
				
			||||||
        UDB::lock_db() | 
					 | 
				
			||||||
            .insert_agent(&msg) | 
					 | 
				
			||||||
            .map(|_| build_ok("")) | 
					 | 
				
			||||||
            .or_else(|e| Ok(build_err(e))) | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn get_agents(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
					    pub async fn get_agents(uid: Option<Uuid>) -> EndpResult<Vec<Agent>> { | 
				
			||||||
        info!("hnd: get_agents"); | 
					        UDB::lock_db().get_agents(uid).map_err(From::from) | 
				
			||||||
        UDB::lock_db() | 
					 | 
				
			||||||
            .get_agents(uid) | 
					 | 
				
			||||||
            .map(|m| build_message(m)) | 
					 | 
				
			||||||
            .or_else(|e| Ok(build_err(e))) | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn get_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
					    pub async fn get_jobs(uid: Option<Uuid>) -> EndpResult<Vec<JobMeta>> { | 
				
			||||||
        info!("hnd: get_jobs"); | 
					        UDB::lock_db().get_jobs(uid).map_err(From::from) | 
				
			||||||
        UDB::lock_db() | 
					 | 
				
			||||||
            .get_jobs(uid) | 
					 | 
				
			||||||
            .map(|m| build_message(m)) | 
					 | 
				
			||||||
            .or_else(|e| Ok(build_err(e))) | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn get_agent_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
					    pub async fn get_agent_jobs(uid: Option<Uuid>) -> EndpResult<Vec<AssignedJob>> { | 
				
			||||||
        info!("hnd: get_agent_jobs"); | 
					 | 
				
			||||||
        UDB::lock_db() | 
					        UDB::lock_db() | 
				
			||||||
            .get_exact_jobs(uid, false) | 
					            .get_exact_jobs(uid, false) | 
				
			||||||
            .map(|m| build_message(m)) | 
					            .map_err(From::from) | 
				
			||||||
            .or_else(|e| Ok(build_err(e))) | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn get_personal_jobs(uid: Option<Uuid>) -> Result<Response<Body>, Rejection> { | 
					    pub async fn get_personal_jobs(uid: Uuid) -> EndpResult<Vec<AssignedJob>> { | 
				
			||||||
        info!("hnd: get_personal_jobs"); | 
					        let db = UDB::lock_db(); | 
				
			||||||
        let agents = UDB::lock_db().get_agents(uid).unwrap(); | 
					        let mut agents = db.get_agents(Some(uid))?; | 
				
			||||||
        if agents.len() == 0 { | 
					        if agents.is_empty() { | 
				
			||||||
            let db = UDB::lock_db(); | 
					            let new_agent = Agent::with_id(uid); | 
				
			||||||
            db.insert_agent(&Agent::with_id(uid.unwrap())).unwrap(); | 
					            db.insert_agent(&new_agent)?; | 
				
			||||||
            let job = db.find_job_by_alias("agent_hello").unwrap(); | 
					            let job = db | 
				
			||||||
            if let Err(e) = db.set_jobs_for_agent(&uid.unwrap(), &[job.id]) { | 
					                .find_job_by_alias("agent_hello")? | 
				
			||||||
                return Ok(build_err(e)); | 
					                .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 = UDB::lock_db().get_exact_jobs(uid, true); | 
					        let result = db.get_exact_jobs(Some(uid), true)?; | 
				
			||||||
        match result { | 
					
 | 
				
			||||||
            Ok(r) => { | 
					        for j in result.iter() { | 
				
			||||||
                let db = UDB::lock_db(); | 
					            db.update_job_status(j.id, JobState::Running)?; | 
				
			||||||
                for j in r.iter() { | 
					 | 
				
			||||||
                    db.update_job_status(j.id, JobState::Running).unwrap(); | 
					 | 
				
			||||||
                } | 
					 | 
				
			||||||
                Ok(build_message(r)) | 
					 | 
				
			||||||
            } | 
					 | 
				
			||||||
            Err(e) => Ok(build_err(e)), | 
					 | 
				
			||||||
        } | 
					        } | 
				
			||||||
 | 
					        Ok(result) | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn upload_jobs( | 
					    pub async fn upload_jobs(msg: BaseMessage<'static, Vec<JobMeta>>) -> EndpResult<Vec<Uuid>> { | 
				
			||||||
        msg: BaseMessage<'static, Vec<JobMeta>>, | 
					 | 
				
			||||||
    ) -> Result<Response<Body>, Rejection> { | 
					 | 
				
			||||||
        info!("hnd: upload_jobs"); | 
					 | 
				
			||||||
        UDB::lock_db() | 
					        UDB::lock_db() | 
				
			||||||
            .insert_jobs(&msg.into_inner()) | 
					            .insert_jobs(&msg.into_inner()) | 
				
			||||||
            .map(|_| build_ok("")) | 
					            .map_err(From::from) | 
				
			||||||
            .or_else(|e| Ok(build_err(e))) | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn del(uid: Uuid) -> Result<Response<Body>, Rejection> { | 
					    pub async fn del(uid: Uuid) -> EndpResult<usize> { | 
				
			||||||
        info!("hnd: del"); | 
					 | 
				
			||||||
        let db = UDB::lock_db(); | 
					        let db = UDB::lock_db(); | 
				
			||||||
        let del_fns = &[UDB::del_agents, UDB::del_jobs, UDB::del_results]; | 
					        let del_fns = &[UDB::del_agents, UDB::del_jobs, UDB::del_results]; | 
				
			||||||
        for del_fn in del_fns { | 
					        for del_fn in del_fns { | 
				
			||||||
            let affected = del_fn(&db, &vec![uid]).unwrap(); | 
					            let affected = del_fn(&db, &[uid])?; | 
				
			||||||
            if affected > 0 { | 
					            if affected > 0 { | 
				
			||||||
                return Ok(build_message(affected as i32)); | 
					                return Ok(affected); | 
				
			||||||
            } | 
					            } | 
				
			||||||
        } | 
					        } | 
				
			||||||
        Ok(build_message(0)) | 
					        Ok(0) | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn set_jobs( | 
					    pub async fn set_jobs( | 
				
			||||||
        agent_uid: Uuid, | 
					        agent_uid: Uuid, | 
				
			||||||
        msg: BaseMessage<'static, Vec<String>>, | 
					        msg: BaseMessage<'static, Vec<String>>, | 
				
			||||||
    ) -> Result<Response<Body>, Rejection> { | 
					    ) -> EndpResult<Vec<Uuid>> { | 
				
			||||||
        info!("hnd: set_jobs_by_alias, agent: {}", agent_uid); | 
					        msg.into_inner() | 
				
			||||||
        let jobs: Result<Vec<Uuid>, ULocalError> = msg | 
					 | 
				
			||||||
            .into_inner() | 
					 | 
				
			||||||
            .into_iter() | 
					            .into_iter() | 
				
			||||||
            .map(|ident| { | 
					            .map(|ident| { | 
				
			||||||
                info!("hnd: set_jobs_by_alias, job: {}", ident); | 
					                Uuid::parse_str(&ident).or_else(|_| { | 
				
			||||||
                Uuid::parse_str(&ident) | 
					                    let job_from_db = UDB::lock_db().find_job_by_alias(&ident); | 
				
			||||||
                    .or_else(|_| UDB::lock_db().find_job_by_alias(&ident).map(|j| j.id)) | 
					                    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(); | 
					            .collect::<Result<Vec<Uuid>, Error>>() | 
				
			||||||
        match jobs { | 
					            .and_then(|j| UDB::lock_db().set_jobs_for_agent(&agent_uid, &j)) | 
				
			||||||
            Ok(j) => UDB::lock_db() | 
					            .map_err(From::from) | 
				
			||||||
                .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)), | 
					 | 
				
			||||||
        } | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub async fn report( | 
					    pub async fn report<Data: OneOrVec<Reportable> + AsMsg + 'static>( | 
				
			||||||
        msg: BaseMessage<'static, Vec<ExecResult>>, | 
					        msg: BaseMessage<'static, Data>, | 
				
			||||||
    ) -> Result<Response<Body>, Rejection> { | 
					    ) -> EndpResult<()> { | 
				
			||||||
        info!("hnd: report"); | 
					 | 
				
			||||||
        let id = msg.id; | 
					        let id = msg.id; | 
				
			||||||
        let mut failed = vec![]; | 
					        for entry in msg.into_inner().into_vec() { | 
				
			||||||
        for entry in msg.into_inner() { | 
					 | 
				
			||||||
            match entry { | 
					            match entry { | 
				
			||||||
                ExecResult::Assigned(res) => { | 
					                Reportable::Assigned(mut result) => { | 
				
			||||||
                    if id != res.agent_id { | 
					                    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; | 
					                        continue; | 
				
			||||||
                    } | 
					                    } | 
				
			||||||
                    let db = UDB::lock_db(); | 
					                    result.state = JobState::Finished; | 
				
			||||||
                    if let Err(e) = res | 
					                    result.touch(); | 
				
			||||||
                        .save_changes::<AssignedJob>(&db.conn) | 
					                    match result.exec_type { | 
				
			||||||
                        .map_err(ULocalError::from) | 
					                        JobType::Init => match &result.result { | 
				
			||||||
                    { | 
					                            Some(rbytes) => { | 
				
			||||||
                        failed.push(e.to_string()) | 
					                                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!(), | 
				
			||||||
                    } | 
					                    } | 
				
			||||||
 | 
					                    UDB::lock_db().update_result(&result)?; | 
				
			||||||
                } | 
					                } | 
				
			||||||
                ExecResult::Agent(mut a) => { | 
					                Reportable::Error(e) => { | 
				
			||||||
                    a.state = AgentState::Active; | 
					                    warn!("{} reported an error: {}", id, e); | 
				
			||||||
                    Self::add_agent(a).await?; | 
					 | 
				
			||||||
                } | 
					                } | 
				
			||||||
                ExecResult::Dummy => (), | 
					                Reportable::Dummy => (), | 
				
			||||||
            } | 
					            } | 
				
			||||||
        } | 
					        } | 
				
			||||||
        if failed.len() > 0 { | 
					        Ok(()) | 
				
			||||||
            let err_msg = ULocalError::ProcessingError(failed.join(", ")); | 
					    } | 
				
			||||||
            return Ok(build_err(err_msg)); | 
					
 | 
				
			||||||
        } | 
					    pub async fn update_agent(agent: BaseMessage<'static, Agent>) -> EndpResult<()> { | 
				
			||||||
        Ok(build_ok("")) | 
					        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; | 
					use u_server_lib::serve; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[macro_use] | 
				
			||||||
 | 
					extern crate tracing; | 
				
			||||||
 | 
					
 | 
				
			||||||
#[tokio::main] | 
					#[tokio::main] | 
				
			||||||
async fn main() { | 
					async fn main() { | 
				
			||||||
    serve().await; | 
					    if let Err(e) = serve().await { | 
				
			||||||
 | 
					        error!("U_SERVER error: {}", e); | 
				
			||||||
 | 
					    } | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
				
			|||||||
@ -1,117 +1,267 @@ | 
				
			|||||||
#[macro_use] | 
					#[macro_use] | 
				
			||||||
extern crate log; | 
					extern crate tracing; | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[cfg(test)] | 
				
			||||||
#[macro_use] | 
					#[macro_use] | 
				
			||||||
extern crate mockall; | 
					extern crate rstest; | 
				
			||||||
#[macro_use] | 
					 | 
				
			||||||
extern crate mockall_double; | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
// because of linking errors
 | 
					// due to linking errors
 | 
				
			||||||
extern crate openssl; | 
					extern crate openssl; | 
				
			||||||
#[macro_use] | 
					// don't touch anything
 | 
				
			||||||
extern crate diesel; | 
					extern crate diesel; | 
				
			||||||
//
 | 
					// in this block
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mod db; | 
					mod db; | 
				
			||||||
mod filters; | 
					mod error; | 
				
			||||||
mod handlers; | 
					mod handlers; | 
				
			||||||
 | 
					
 | 
				
			||||||
use db::UDB; | 
					use error::{Error as ServerError, RejResponse}; | 
				
			||||||
use filters::make_filters; | 
					use serde::{de::DeserializeOwned, Deserialize}; | 
				
			||||||
use u_lib::{config::MASTER_PORT, models::*, utils::init_env}; | 
					use std::{convert::Infallible, path::PathBuf}; | 
				
			||||||
use warp::Filter; | 
					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, | 
				
			||||||
 | 
					}; | 
				
			||||||
 | 
					
 | 
				
			||||||
const LOGFILE: &str = "u_server.log"; | 
					use crate::db::UDB; | 
				
			||||||
 | 
					use crate::handlers::Endpoints; | 
				
			||||||
 | 
					
 | 
				
			||||||
fn prefill_jobs() { | 
					#[derive(Deserialize)] | 
				
			||||||
    let agent_hello = JobMeta::builder() | 
					struct ServEnv { | 
				
			||||||
        .with_type(misc::JobType::Manage) | 
					    admin_auth_token: String, | 
				
			||||||
        .with_alias("agent_hello") | 
					 | 
				
			||||||
        .build() | 
					 | 
				
			||||||
        .unwrap(); | 
					 | 
				
			||||||
    UDB::lock_db().insert_jobs(&[agent_hello]).ok(); | 
					 | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
fn init_logger() { | 
					fn get_content<M>() -> impl Filter<Extract = (BaseMessage<'static, M>,), Error = Rejection> + Clone | 
				
			||||||
    use simplelog::*; | 
					where | 
				
			||||||
    use std::fs::OpenOptions; | 
					    M: AsMsg + Sync + Send + DeserializeOwned + 'static, | 
				
			||||||
    let log_cfg = ConfigBuilder::new() | 
					{ | 
				
			||||||
        .set_time_format_str("%x %X") | 
					    body::content_length_limit(1024 * 64).and(body::json::<BaseMessage<M>>()) | 
				
			||||||
        .set_time_to_local(true) | 
					} | 
				
			||||||
        .build(); | 
					
 | 
				
			||||||
    let logfile = OpenOptions::new() | 
					fn into_message<M: AsMsg>(msg: M) -> Json { | 
				
			||||||
        .append(true) | 
					    json(&msg.as_message()) | 
				
			||||||
        .create(true) | 
					} | 
				
			||||||
        .open(LOGFILE) | 
					
 | 
				
			||||||
        .unwrap(); | 
					pub fn init_endpoints( | 
				
			||||||
    let level = LevelFilter::Info; | 
					    auth_token: &str, | 
				
			||||||
    let loggers = vec![ | 
					) -> impl Filter<Extract = (impl Reply,), Error = Rejection> + Clone { | 
				
			||||||
        WriteLogger::new(level, log_cfg.clone(), logfile) as Box<dyn SharedLogger>, | 
					    let path = |p: &'static str| warp::post().and(warp::path(p)); | 
				
			||||||
        TermLogger::new(level, log_cfg, TerminalMode::Stderr, ColorChoice::Auto), | 
					    let infallible_none = |_| async { Ok::<_, Infallible>((None::<Uuid>,)) }; | 
				
			||||||
    ]; | 
					
 | 
				
			||||||
    CombinedLogger::init(loggers).unwrap(); | 
					    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); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    let report = path("report") | 
				
			||||||
 | 
					        .and(get_content::<Vec<Reportable>>()) | 
				
			||||||
 | 
					        .and_then(Endpoints::report) | 
				
			||||||
 | 
					        .map(ok); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    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) | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
fn init_all() { | 
					pub fn preload_jobs() -> Result<(), ServerError> { | 
				
			||||||
    init_logger(); | 
					    let job_alias = "agent_hello"; | 
				
			||||||
    init_env(); | 
					    let if_job_exists = UDB::lock_db().find_job_by_alias(job_alias)?; | 
				
			||||||
    prefill_jobs(); | 
					    if if_job_exists.is_none() { | 
				
			||||||
 | 
					        let agent_hello = JobMeta::builder() | 
				
			||||||
 | 
					            .with_type(JobType::Init) | 
				
			||||||
 | 
					            .with_alias(job_alias) | 
				
			||||||
 | 
					            .build() | 
				
			||||||
 | 
					            .unwrap(); | 
				
			||||||
 | 
					        UDB::lock_db().insert_jobs(&[agent_hello])?; | 
				
			||||||
 | 
					    } | 
				
			||||||
 | 
					    Ok(()) | 
				
			||||||
} | 
					} | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn serve() { | 
					pub async fn serve() -> Result<(), ServerError> { | 
				
			||||||
    init_all(); | 
					    init_logger(Some("u_server")); | 
				
			||||||
    let routes = make_filters(); | 
					    preload_jobs()?; | 
				
			||||||
    warp::serve(routes.with(warp::log("warp"))) | 
					
 | 
				
			||||||
 | 
					    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)); | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    warp::serve(routes) | 
				
			||||||
        .tls() | 
					        .tls() | 
				
			||||||
        .cert_path("./certs/server.crt") | 
					        .cert_path(certs_dir.join("server.crt")) | 
				
			||||||
        .key_path("./certs/server.key") | 
					        .key_path(certs_dir.join("server.key")) | 
				
			||||||
        .client_auth_required_path("./certs/ca.crt") | 
					        .client_auth_required_path(certs_dir.join("ca.crt")) | 
				
			||||||
        .run(([0, 0, 0, 0], MASTER_PORT)) | 
					        .run(([0, 0, 0, 0], MASTER_PORT)) | 
				
			||||||
        .await; | 
					        .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)] | 
					#[cfg(test)] | 
				
			||||||
mod tests { | 
					mod tests { | 
				
			||||||
    use super::*; | 
					    use super::*; | 
				
			||||||
    #[double] | 
					 | 
				
			||||||
    use crate::handlers::Endpoints; | 
					    use crate::handlers::Endpoints; | 
				
			||||||
    use handlers::build_ok; | 
					    use handlers::build_ok; | 
				
			||||||
    use mockall::predicate::*; | 
					    use u_lib::messaging::{AsMsg, BaseMessage, Reportable}; | 
				
			||||||
    use test_case::test_case; | 
					 | 
				
			||||||
    use u_lib::messaging::{AsMsg, BaseMessage}; | 
					 | 
				
			||||||
    use uuid::Uuid; | 
					    use uuid::Uuid; | 
				
			||||||
    use warp::test::request; | 
					    use warp::test; | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[test_case(Some(Uuid::new_v4()))] | 
					    #[rstest] | 
				
			||||||
    #[test_case(None => panics)] | 
					    #[case(Some(Uuid::new_v4()))] | 
				
			||||||
 | 
					    #[should_panic] | 
				
			||||||
 | 
					    #[case(None)] | 
				
			||||||
    #[tokio::test] | 
					    #[tokio::test] | 
				
			||||||
    async fn test_get_agent_jobs_unauthorized(uid: Option<Uuid>) { | 
					    async fn test_get_agent_jobs_unauthorized(#[case] uid: Option<Uuid>) { | 
				
			||||||
        let mock = Endpoints::get_agent_jobs_context(); | 
					        let mock = Endpoints::faux(); | 
				
			||||||
        mock.expect().with(eq(uid)).returning(|_| Ok(build_ok(""))); | 
					        when!(mock.get_agent_jobs).then_return(Ok(build_ok(""))); | 
				
			||||||
        request() | 
					        //mock.expect().with(eq(uid)).returning(|_| Ok(build_ok("")));
 | 
				
			||||||
 | 
					        test::request() | 
				
			||||||
            .path(&format!( | 
					            .path(&format!( | 
				
			||||||
                "/get_agent_jobs/{}", | 
					                "/get_agent_jobs/{}", | 
				
			||||||
                uid.map(|u| u.simple().to_string()).unwrap_or(String::new()) | 
					                uid.map(|u| u.simple().to_string()).unwrap_or(String::new()) | 
				
			||||||
            )) | 
					            )) | 
				
			||||||
            .method("GET") | 
					            .method("GET") | 
				
			||||||
            .filter(&make_filters()) | 
					            .filter(&init_filters("")) | 
				
			||||||
            .await | 
					            .await | 
				
			||||||
            .unwrap(); | 
					            .unwrap(); | 
				
			||||||
        mock.checkpoint(); | 
					 | 
				
			||||||
    } | 
					    } | 
				
			||||||
 | 
					
 | 
				
			||||||
    #[tokio::test] | 
					    #[tokio::test] | 
				
			||||||
    async fn test_report_unauth_successful() { | 
					    async fn test_report_unauth_successful() { | 
				
			||||||
        let mock = Endpoints::report_context(); | 
					        let mock = Endpoints::report(); | 
				
			||||||
        mock.expect() | 
					        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(""))); | 
					            .returning(|_| Ok(build_ok(""))); | 
				
			||||||
        request() | 
					        test::request() | 
				
			||||||
            .path("/report/") | 
					            .path("/report/") | 
				
			||||||
            .method("POST") | 
					            .method("POST") | 
				
			||||||
            .json(&vec![ExecResult::Dummy].as_message()) | 
					            .json(&vec![Reportable::Dummy].as_message()) | 
				
			||||||
            .filter(&make_filters()) | 
					            .filter(&init_filters("")) | 
				
			||||||
            .await | 
					            .await | 
				
			||||||
            .unwrap(); | 
					            .unwrap(); | 
				
			||||||
        mock.checkpoint(); | 
					        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 | 
					#!/bin/bash | 
				
			||||||
set -e | 
					set -e | 
				
			||||||
 | 
					export DOCKER_UID=$(id -u) | 
				
			||||||
 | 
					export DOCKER_GID=$(id -g) | 
				
			||||||
 | 
					[[ "$@" =~ "--release" ]] && export PROFILE=release || export PROFILE=debug | 
				
			||||||
python integration_tests.py $@ | 
					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 mod panel; | 
				
			||||||
 | 
					
 | 
				
			||||||
pub use panel::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