add payload-overview component & use try_from rawjob instead of builder

pull/9/head
plazmoid 1 year ago
parent 45bba0dd9b
commit 69b1d3d901
  1. 8
      Cargo.lock
  2. 14
      bin/u_panel/src/argparse.rs
  3. 4
      bin/u_panel/src/gui/fe/src/app/app.module.ts
  4. 22
      bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.html
  5. 29
      bin/u_panel/src/gui/fe/src/app/components/dialogs/job-info-dialog/job-info-dialog.component.ts
  6. 13
      bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.html
  7. 5
      bin/u_panel/src/gui/fe/src/app/components/dialogs/payload-info-dialog/payload-info-dialog.component.ts
  8. 9
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.html
  9. 0
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.less
  10. 22
      bin/u_panel/src/gui/fe/src/app/components/payload-overview/payload-overview.component.ts
  11. 4
      bin/u_panel/src/gui/fe/src/app/components/tables/agent-table/agent-table.component.ts
  12. 5
      bin/u_panel/src/gui/fe/src/app/components/tables/base-table/base-table.component.ts
  13. 76
      bin/u_panel/src/gui/fe/src/app/components/tables/job-table/job-table.component.ts
  14. 6
      bin/u_panel/src/gui/fe/src/app/components/tables/payload-table/payload-table.component.ts
  15. 4
      bin/u_panel/src/gui/fe/src/app/components/tables/result-table/result-table.component.ts
  16. 4
      bin/u_panel/src/gui/fe/src/app/models/index.ts
  17. 6
      bin/u_panel/src/gui/fe/src/app/models/job.model.ts
  18. 23
      bin/u_panel/src/gui/fe/src/app/services/api.service.ts
  19. 20
      bin/u_server/src/db.rs
  20. 14
      bin/u_server/src/handlers.rs
  21. 8
      bin/u_server/src/u_server.rs
  22. 4
      integration-tests/tests/fixtures/agent.rs
  23. 8
      integration-tests/tests/integration_tests/behaviour.rs
  24. 16
      integration-tests/tests/integration_tests/endpoints.rs
  25. 45
      lib/u_lib/src/api.rs
  26. 2
      lib/u_lib/src/cache.rs
  27. 40
      lib/u_lib/src/jobs.rs
  28. 2
      lib/u_lib/src/messaging.rs
  29. 6
      lib/u_lib/src/models/jobs/assigned.rs
  30. 298
      lib/u_lib/src/models/jobs/meta.rs
  31. 3
      lib/u_lib/src/models/jobs/misc.rs
  32. 10
      lib/u_lib/src/models/payload.rs

8
Cargo.lock generated

@ -1700,9 +1700,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "pkg-config" name = "pkg-config"
version = "0.3.26" version = "0.3.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964"
[[package]] [[package]]
name = "platforms" name = "platforms"
@ -2024,9 +2024,9 @@ dependencies = [
[[package]] [[package]]
name = "rustix" name = "rustix"
version = "0.37.17" version = "0.37.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc809f704c03a812ac71f22456c857be34185cac691a4316f27ab0f633bb9009" checksum = "8bbfc1d1c7c40c01715f47d71444744a81669ca84e8b63e25a55e169b1f86433"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"errno 0.3.1", "errno 0.3.1",

@ -99,7 +99,7 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
CRUD::Create { item: job } => { CRUD::Create { item: job } => {
let raw_job = from_str::<RawJob>(&job) let raw_job = from_str::<RawJob>(&job)
.map_err(|e| UError::DeserializeError(e.to_string(), job))?; .map_err(|e| UError::DeserializeError(e.to_string(), job))?;
let mut job = raw_job.validated()?; let mut job = raw_job.try_into_job()?;
if let Some(payload) = &mut job.payload { if let Some(payload) = &mut job.payload {
payload.join_payload()?; payload.join_payload()?;
@ -112,13 +112,13 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
None => into_value(client.get_jobs().await?), None => into_value(client.get_jobs().await?),
}, },
CRUD::RUD(RUD::Update { item }) => { CRUD::RUD(RUD::Update { item }) => {
let raw_job = from_str::<RawJob>(&item) let raw_job = from_str::<JobMeta>(&item)
.map_err(|e| UError::DeserializeError(e.to_string(), item))?; .map_err(|e| UError::DeserializeError(e.to_string(), item))?;
let mut job = raw_job.validated()?; let job = raw_job.validate()?;
if let Some(payload) = &mut job.payload { // if let Some(payload) = &mut job.payload {
payload.join_payload()?; // payload.join_payload()?;
} // }
into_value(client.update_job(&job).await?) into_value(client.update_job(&job).await?)
} }
@ -145,7 +145,7 @@ pub async fn process_cmd(client: HttpClient, args: Args) -> PanelResult<Value> {
} }
PayloadCRUD::Read { id } => match id { PayloadCRUD::Read { id } => match id {
None => into_value(client.get_payloads().await?), None => into_value(client.get_payloads().await?),
Some(id) => into_value(client.get_payload(id, args.brief).await?), Some(id) => into_value(vec![client.get_payload(id, args.brief).await?]),
}, },
PayloadCRUD::Update { item } => { PayloadCRUD::Update { item } => {
let payload = from_str::<Payload>(&item) let payload = from_str::<Payload>(&item)

@ -27,6 +27,7 @@ import { MatTooltipModule } from '@angular/material/tooltip';
import { MatSnackBarModule } from '@angular/material/snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatListModule } from '@angular/material/list'; import { MatListModule } from '@angular/material/list';
import { GlobalErrorComponent } from './components/global-error/global-error.component'; import { GlobalErrorComponent } from './components/global-error/global-error.component';
import { PayloadOverviewComponent } from './components/payload-overview/payload-overview.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
@ -40,7 +41,8 @@ import { GlobalErrorComponent } from './components/global-error/global-error.com
AssignJobDialogComponent, AssignJobDialogComponent,
PayloadComponent, PayloadComponent,
PayloadInfoDialogComponent, PayloadInfoDialogComponent,
GlobalErrorComponent GlobalErrorComponent,
PayloadOverviewComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,

@ -4,46 +4,40 @@
<div class="info-dialog-forms-box-smol"> <div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>ID</mat-label> <mat-label>ID</mat-label>
<input matInput disabled value="{{data.job.id}}"> <input matInput disabled value="{{data.meta.id}}">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Alias</mat-label> <mat-label>Alias</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.alias"> <input matInput [readonly]="isPreview" [(ngModel)]="data.meta.alias">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Args</mat-label> <mat-label>Args</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.argv"> <input matInput [readonly]="isPreview" [(ngModel)]="data.meta.argv">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="info-dialog-forms-box-smol"> <div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Type</mat-label> <mat-label>Type</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.exec_type"> <input matInput [readonly]="isPreview" [(ngModel)]="data.meta.exec_type">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Platform</mat-label> <mat-label>Platform</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.target_platforms"> <input matInput [readonly]="isPreview" [(ngModel)]="data.meta.target_platforms">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Schedule</mat-label> <mat-label>Schedule</mat-label>
<input matInput [readonly]="isPreview" [(ngModel)]="data.job.schedule"> <input matInput [readonly]="isPreview" [(ngModel)]="data.meta.schedule">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="info-dialog-forms-box"> <div class="info-dialog-forms-box">
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Payload</mat-label> <mat-label>Payload</mat-label>
<mat-select [disabled]="isPreview" [(value)]="data.job.payload"> <mat-select [disabled]="isPreview" [(value)]="data.meta.payload_id">
<mat-option *ngFor="let pld of allPayloads" [value]="pld[0]">{{ pld[1] }}</mat-option> <mat-option *ngFor="let pld of allPayloads" [value]="pld[0]">{{ pld[1] }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field" floatLabel="always">
<mat-label>Payload data</mat-label>
<textarea matInput cdkTextareaAutosize *ngIf="!isTooBigPayload" [readonly]="isPreview"
[(ngModel)]="decodedPayload">
</textarea>
<input matInput *ngIf="isTooBigPayload" disabled placeholder="Payload is too big to display">
</mat-form-field>
</div> </div>
<payload-overview *ngIf="data.payload" [preview]="true" [payload]="data.payload"></payload-overview>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-raised-button *ngIf="isPreview" (click)="isPreview = false">Edit</button> <button mat-raised-button *ngIf="isPreview" (click)="isPreview = false">Edit</button>

@ -1,7 +1,7 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { MAT_DIALOG_DATA } from '@angular/material/dialog'; import { MAT_DIALOG_DATA } from '@angular/material/dialog';
import { EventEmitter } from '@angular/core'; import { EventEmitter } from '@angular/core';
import { Job } from '../../../models/job.model'; import { Job, JobModel } from '../../../models/job.model';
import { ApiTableService } from 'src/app/services'; import { ApiTableService } from 'src/app/services';
import { PayloadModel } from 'src/app/models'; import { PayloadModel } from 'src/app/models';
@ -11,36 +11,19 @@ import { PayloadModel } from 'src/app/models';
styleUrls: ['../base-info-dialog.component.less'] styleUrls: ['../base-info-dialog.component.less']
}) })
export class JobInfoDialogComponent { export class JobInfoDialogComponent {
isPreview = true;
isTooBigPayload = false;
decodedPayload = "";
//[id, name] //[id, name]
allPayloads: [string, string][] = []; isPreview = true;
allPayloads: [string | null, string][] = [[null, "none"]];
onSave = new EventEmitter(); onSave = new EventEmitter<JobModel>();
constructor(@Inject(MAT_DIALOG_DATA) public data: Job, dataSource: ApiTableService) { constructor(@Inject(MAT_DIALOG_DATA) public data: Job, dataSource: ApiTableService) {
if (data.payload !== null) {
this.showPayload(data.payload)
}
dataSource.getPayloads().subscribe(resp => { dataSource.getPayloads().subscribe(resp => {
this.allPayloads = resp.map(r => [r.id, r.name]) this.allPayloads = this.allPayloads.concat(resp.map(r => [r.id, r.name]))
}) })
} }
showPayload(payload: PayloadModel) {
if (payload.data !== null) {
this.decodedPayload = new TextDecoder().decode(new Uint8Array(payload.data))
} else {
this.isTooBigPayload = true
}
}
updateJob() { updateJob() {
// if (this.decodedPayload.length > 0) { this.onSave.emit(this.data.meta);
// this.data.payload = Array.from(new TextEncoder().encode(this.decodedPayload))
// }
// this.onSave.emit(this.data);
} }
} }

@ -1,23 +1,26 @@
<h2 mat-dialog-title>Result</h2> <h2 mat-dialog-title>Payload</h2>
<mat-dialog-content> <mat-dialog-content>
<div class="info-dialog-forms-box-smol"> <div class="info-dialog-forms-box-smol">
<mat-form-field class="info-dlg-field" cdkFocusInitial> <mat-form-field class="info-dlg-field" cdkFocusInitial>
<mat-label>ID</mat-label> <mat-label>ID</mat-label>
<input matInput readonly value="{{data.id}}"> <input matInput readonly value="{{payload.id}}">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Name</mat-label> <mat-label>Name</mat-label>
<input matInput value="{{data.name}}"> <input matInput value="{{payload.name}}">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>MIME-type</mat-label> <mat-label>MIME-type</mat-label>
<input matInput value="{{data.mime_type}}"> <input matInput value="{{payload.mime_type}}">
</mat-form-field> </mat-form-field>
<mat-form-field class="info-dlg-field"> <mat-form-field class="info-dlg-field">
<mat-label>Size</mat-label> <mat-label>Size</mat-label>
<input matInput value="{{data.size}}"> <input matInput value="{{payload.size}}">
</mat-form-field> </mat-form-field>
</div> </div>
<div class="info-dialog-forms-box">
<payload-overview [preview]="true" [payload]="payload"></payload-overview>
</div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions align="end"> <mat-dialog-actions align="end">
<button mat-button mat-dialog-close>Close</button> <button mat-button mat-dialog-close>Close</button>

@ -5,10 +5,9 @@ import { PayloadModel } from 'src/app/models/payload.model';
@Component({ @Component({
selector: 'payload-info-dialog', selector: 'payload-info-dialog',
templateUrl: 'payload-info-dialog.component.html', templateUrl: 'payload-info-dialog.component.html',
styleUrls: [] styleUrls: ['../base-info-dialog.component.less']
}) })
export class PayloadInfoDialogComponent { export class PayloadInfoDialogComponent {
constructor(@Inject(MAT_DIALOG_DATA) public data: PayloadModel) { } constructor(@Inject(MAT_DIALOG_DATA) public payload: PayloadModel) { }
} }

@ -0,0 +1,9 @@
<div class="info-dialog-forms-box">
<mat-form-field class="info-dlg-field" floatLabel="always">
<mat-label>Payload data</mat-label>
<textarea matInput cdkTextareaAutosize *ngIf="!isTooBigPayload" [readonly]="isPreview"
[(ngModel)]="decodedPayload">
</textarea>
<input matInput *ngIf="isTooBigPayload" disabled placeholder="Payload is too big to display">
</mat-form-field>
</div>

@ -0,0 +1,22 @@
import { Component, Input, OnInit } from '@angular/core';
import { PayloadModel } from 'src/app/models';
@Component({
selector: 'payload-overview',
templateUrl: './payload-overview.component.html',
styleUrls: ['./payload-overview.component.less']
})
export class PayloadOverviewComponent implements OnInit {
@Input() payload!: PayloadModel;
@Input("preview") isPreview = true;
isTooBigPayload = false;
decodedPayload = "";
ngOnInit() {
if (this.payload.data !== null) {
this.decodedPayload = new TextDecoder().decode(new Uint8Array(this.payload.data))
} else {
this.isTooBigPayload = true
}
}
}

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TablesComponent } from '../base-table/base-table.component'; import { TableComponent } from '../base-table/base-table.component';
import { AgentModel, Area } from '../../../models'; import { AgentModel, Area } from '../../../models';
import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialogs'; import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialogs';
@ -8,7 +8,7 @@ import { AssignJobDialogComponent, AgentInfoDialogComponent } from '../../dialog
templateUrl: './agent-table.component.html', templateUrl: './agent-table.component.html',
styleUrls: ['../base-table/base-table.component.less'], styleUrls: ['../base-table/base-table.component.less'],
}) })
export class AgentComponent extends TablesComponent<AgentModel> implements OnInit { export class AgentComponent extends TableComponent<AgentModel> implements OnInit {
area = 'agents' as Area area = 'agents' as Area
displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions'] displayedColumns = ['id', 'alias', 'username', 'hostname', 'last_active', 'actions']

@ -6,7 +6,7 @@ import { ApiModel, Area } from '../../../models';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@Directive() @Directive()
export abstract class TablesComponent<T extends ApiModel> implements OnInit { export abstract class TableComponent<T extends ApiModel> implements OnInit {
abstract area: Area; abstract area: Area;
table_data: MatTableDataSource<T> = new MatTableDataSource; table_data: MatTableDataSource<T> = new MatTableDataSource;
isLoadingResults = true; isLoadingResults = true;
@ -49,7 +49,8 @@ export abstract class TablesComponent<T extends ApiModel> implements OnInit {
deleteItem(id: string) { deleteItem(id: string) {
if (confirm(`Delete ${id}?`)) { if (confirm(`Delete ${id}?`)) {
this.dataSource.delete(id, this.area) this.dataSource.delete(id, this.area).subscribe(_ => { })
this.loadTableData()
} }
} }

@ -1,7 +1,8 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TablesComponent } from '../base-table/base-table.component'; import { TableComponent } from '../base-table/base-table.component';
import { Area, JobModel } from '../../../models'; import { Area, JobModel, Job } from '../../../models';
import { JobInfoDialogComponent } from '../../dialogs'; import { JobInfoDialogComponent } from '../../dialogs';
import { Observable } from 'rxjs';
@Component({ @Component({
selector: 'job-table', selector: 'job-table',
@ -9,42 +10,63 @@ import { JobInfoDialogComponent } from '../../dialogs';
styleUrls: ['../base-table/base-table.component.less'], styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'jobs' }] providers: [{ provide: 'area', useValue: 'jobs' }]
}) })
export class JobComponent extends TablesComponent<JobModel> { export class JobComponent extends TableComponent<JobModel> {
area = 'jobs' as Area; area = 'jobs' as Area;
displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions'] displayedColumns = ['id', 'alias', 'platform', 'schedule', 'exec_type', 'actions']
showItemDialog(id: string | null) { showItemDialog(id: string | null) {
const show_dlg = (id: string, edit: boolean) => { const is_new_job = id === null;
this.dataSource.getJob(id).subscribe(resp => {
var dialog = this.infoDialog.open(JobInfoDialogComponent, { var dialogData$: Observable<Job>;
data: resp,
width: '1000px', if (is_new_job) {
}); dialogData$ = new Observable(subscriber => {
if (edit) { var defaultJob: Job = {
dialog.componentInstance.isPreview = false meta: {
} alias: null,
argv: '',
exec_type: 'shell',
target_platforms: '*',
payload_id: null,
schedule: null
},
payload: null
};
subscriber.next(defaultJob)
})
} else {
dialogData$ = this.dataSource.getJob(id)
}
dialogData$.subscribe(dialogData => {
const dialog = this.infoDialog.open(JobInfoDialogComponent, {
data: dialogData,
width: '1000px',
});
dialog.componentInstance.isPreview = !is_new_job;
const saveSub = dialog.componentInstance.onSave.subscribe(result => { const saveSub = dialog.componentInstance.onSave.subscribe(result => {
if (is_new_job) {
this.dataSource.create(dialogData.meta, this.area)
.subscribe(_ => {
alert("Created")
this.loadTableData()
})
} else {
this.dataSource.updateJob(result) this.dataSource.updateJob(result)
.subscribe(_ => { .subscribe(_ => {
alert("Saved") alert("Updated")
this.loadTableData() this.loadTableData()
}) })
}) }
dialog.close()
dialog.afterClosed().subscribe(result => {
saveSub.unsubscribe()
this.router.navigate(['.'], { relativeTo: this.route })
})
}) })
}
if (id) { dialog.afterClosed().subscribe(result => {
show_dlg(id, false) saveSub.unsubscribe()
} else { this.router.navigate(['.'], { relativeTo: this.route })
this.dataSource.create(null, this.area).subscribe(resp => {
show_dlg(resp[0], true)
}) })
} })
} }
} }

@ -2,7 +2,7 @@ import { Component } from '@angular/core';
import { Area } from 'src/app/models'; import { Area } from 'src/app/models';
import { PayloadModel } from 'src/app/models/payload.model'; import { PayloadModel } from 'src/app/models/payload.model';
import { PayloadInfoDialogComponent } from '../../dialogs'; import { PayloadInfoDialogComponent } from '../../dialogs';
import { TablesComponent } from '../base-table/base-table.component'; import { TableComponent } from '../base-table/base-table.component';
@Component({ @Component({
selector: 'payload-table', selector: 'payload-table',
@ -10,9 +10,9 @@ import { TablesComponent } from '../base-table/base-table.component';
styleUrls: ['../base-table/base-table.component.less'], styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'payloads' }] providers: [{ provide: 'area', useValue: 'payloads' }]
}) })
export class PayloadComponent extends TablesComponent<PayloadModel> { export class PayloadComponent extends TableComponent<PayloadModel> {
area = 'payloads' as Area area = 'payloads' as Area
displayedColumns = ["name", "mime_type", "size"]; displayedColumns = ["name", "mime_type", "size", 'actions'];
showItemDialog(id: string) { showItemDialog(id: string) {
this.dataSource.getPayload(id).subscribe(resp => { this.dataSource.getPayload(id).subscribe(resp => {

@ -1,5 +1,5 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { TablesComponent } from '../base-table/base-table.component'; import { TableComponent } from '../base-table/base-table.component';
import { Area, ResultModel } from '../../../models'; import { Area, ResultModel } from '../../../models';
import { ResultInfoDialogComponent } from '../../dialogs'; import { ResultInfoDialogComponent } from '../../dialogs';
@ -9,7 +9,7 @@ import { ResultInfoDialogComponent } from '../../dialogs';
styleUrls: ['../base-table/base-table.component.less'], styleUrls: ['../base-table/base-table.component.less'],
providers: [{ provide: 'area', useValue: 'map' }] providers: [{ provide: 'area', useValue: 'map' }]
}) })
export class ResultComponent extends TablesComponent<ResultModel> { export class ResultComponent extends TableComponent<ResultModel> {
area = 'map' as Area area = 'map' as Area
displayedColumns = [ displayedColumns = [
'id', 'id',

@ -18,7 +18,3 @@ export type Area = "agents" | "jobs" | "map" | "payloads";
export type ApiModel = AgentModel | JobModel | ResultModel | PayloadModel | Empty; export type ApiModel = AgentModel | JobModel | ResultModel | PayloadModel | Empty;
export interface Empty { } export interface Empty { }
export function getAreaByModel(_: AgentModel): Area {
return "agents"
}

@ -3,14 +3,14 @@ import { PayloadModel } from './'
export interface JobModel { export interface JobModel {
alias: string | null, alias: string | null,
argv: string, argv: string,
id: string, id?: string,
exec_type: string, exec_type: string,
target_platforms: string, target_platforms: string,
payload: string | null, payload_id: string | null,
schedule: string | null, schedule: string | null,
} }
export interface Job { export interface Job {
job: JobModel, meta: JobModel,
payload: PayloadModel | null, payload: PayloadModel | null,
} }

@ -1,7 +1,7 @@
import { environment } from 'src/environments/environment'; import { environment } from 'src/environments/environment';
import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, map, catchError, throwError } from 'rxjs'; import { Observable, map, catchError, throwError } from 'rxjs';
import { ApiModel, getAreaByModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job } from '../models'; import { ApiModel, PayloadModel, Empty, Area, AgentModel, JobModel, ResultModel, Job } from '../models';
import { Injectable, Inject } from '@angular/core'; import { Injectable, Inject } from '@angular/core';
import { ErrorService } from './error.service'; import { ErrorService } from './error.service';
@ -26,9 +26,7 @@ export class ApiTableService {
requestUrl = `${environment.server}/cmd/`; requestUrl = `${environment.server}/cmd/`;
req<R extends ApiModel>(cmd: string): Observable<ServerResponse<R>> { req<R extends ApiModel>(cmd: string): Observable<ServerResponse<R>> {
return this.http.post<ServerResponse<R>>(this.requestUrl, cmd).pipe( return this.http.post<ServerResponse<R>>(this.requestUrl, cmd)
catchError(this.errorHandler)
)
} }
getOne<T extends ApiModel>(id: string, area: Area, brief: 'yes' | 'no' | 'auto' | null = null): Observable<T> { getOne<T extends ApiModel>(id: string, area: Area, brief: 'yes' | 'no' | 'auto' | null = null): Observable<T> {
@ -73,11 +71,11 @@ export class ApiTableService {
return this.getMany('agents') return this.getMany('agents')
} }
getJobs(): Observable<AgentModel[]> { getJobs(): Observable<JobModel[]> {
return this.getMany('jobs') return this.getMany('jobs')
} }
getResults(): Observable<AgentModel[]> { getResults(): Observable<ResultModel[]> {
return this.getMany('map') return this.getMany('map')
} }
@ -109,11 +107,12 @@ export class ApiTableService {
return this.filterErrStatus(this.req(`${area} delete ${id}`)) return this.filterErrStatus(this.req(`${area} delete ${id}`))
} }
create(item: string | null, area: Area): Observable<string[]> { create<T extends ApiModel>(item: T | null, area: Area): Observable<string[]> {
if (!item) { var serialized = '"{}"'
item = '"{}"' if (item) {
serialized = JSON.stringify(item);
} }
return this.filterErrStatus(this.req(`${area} create ${item}`)) return this.filterErrStatus(this.req(`${area} create '${serialized}'`))
} }
createResult(item: string): Observable<string[]> { createResult(item: string): Observable<string[]> {
@ -131,8 +130,8 @@ export class ApiTableService {
catchError(this.errorHandler.bind(this))) catchError(this.errorHandler.bind(this)))
} }
errorHandler<R>(err: HttpErrorResponse, _: R) { errorHandler(err: HttpErrorResponse, caught: any) {
this.errorService.handle(err.message); this.errorService.handle(caught.data ?? err.message);
return throwError(() => new Error(err.message)); return throwError(() => new Error(err.message));
} }
} }

@ -3,7 +3,7 @@ use diesel::{pg::PgConnection, prelude::*, result::Error as DslError, Connection
use std::mem::drop; use std::mem::drop;
use u_lib::{ use u_lib::{
db::PgAsyncPool, db::PgAsyncPool,
models::{schema, Agent, AssignedJob, Job, JobModel, JobState, Payload}, models::{schema, Agent, AssignedJob, Job, JobMeta, JobState, Payload},
platform::Platform, platform::Platform,
types::Id, types::Id,
}; };
@ -50,7 +50,7 @@ pub struct UDB<'c> {
} }
impl UDB<'_> { impl UDB<'_> {
pub fn insert_jobs(&mut self, jobs: &[JobModel]) -> Result<()> { pub fn insert_jobs(&mut self, jobs: &[JobMeta]) -> Result<()> {
use schema::jobs; use schema::jobs;
diesel::insert_into(jobs::table) diesel::insert_into(jobs::table)
@ -67,7 +67,7 @@ impl UDB<'_> {
.values(payloads) .values(payloads)
.execute(self.conn) .execute(self.conn)
.map(drop) .map(drop)
.map_err(with_err_ctx("Can't insert payloads")) .map_err(with_err_ctx(format!("Can't insert payloads {payloads:?}")))
} }
pub fn get_job(&mut self, id: Id) -> Result<Option<Job>> { pub fn get_job(&mut self, id: Id) -> Result<Option<Job>> {
@ -76,14 +76,14 @@ impl UDB<'_> {
let maybe_job_with_payload = jobs::table let maybe_job_with_payload = jobs::table
.left_join(payloads::table) .left_join(payloads::table)
.filter(jobs::id.eq(id)) .filter(jobs::id.eq(id))
.first::<(JobModel, Option<Payload>)>(self.conn) .first::<(JobMeta, Option<Payload>)>(self.conn)
.optional() .optional()
.map_err(with_err_ctx(format!("Can't get job {id}")))?; .map_err(with_err_ctx(format!("Can't get job {id}")))?;
Ok(maybe_job_with_payload.map(|(job, payload)| Job { job, payload })) Ok(maybe_job_with_payload.map(|(job, payload)| Job { meta: job, payload }))
} }
pub fn get_jobs(&mut self) -> Result<Vec<JobModel>> { pub fn get_jobs(&mut self) -> Result<Vec<JobMeta>> {
use schema::jobs; use schema::jobs;
jobs::table jobs::table
@ -125,12 +125,12 @@ impl UDB<'_> {
let maybe_job_with_payload = jobs::table let maybe_job_with_payload = jobs::table
.left_join(payloads::table) .left_join(payloads::table)
.filter(jobs::alias.eq(alias)) .filter(jobs::alias.eq(alias))
.first::<(JobModel, Option<Payload>)>(self.conn) .first::<(JobMeta, Option<Payload>)>(self.conn)
.optional() .optional()
.map_err(with_err_ctx(format!("Can't get job by alias {alias}")))?; .map_err(with_err_ctx(format!("Can't get job by alias {alias}")))?;
Ok(maybe_job_with_payload.map(|(job, payload_meta)| Job { Ok(maybe_job_with_payload.map(|(job, payload_meta)| Job {
job, meta: job,
payload: payload_meta, payload: payload_meta,
})) }))
} }
@ -303,8 +303,8 @@ impl UDB<'_> {
Ok(()) Ok(())
} }
pub fn update_job(&mut self, job: &JobModel) -> Result<()> { pub fn update_job(&mut self, job: &JobMeta) -> Result<()> {
job.save_changes::<JobModel>(self.conn) job.save_changes::<JobMeta>(self.conn)
.map_err(with_err_ctx(format!("Can't update job {job:?}")))?; .map_err(with_err_ctx(format!("Can't update job {job:?}")))?;
Ok(()) Ok(())
} }

@ -134,7 +134,7 @@ impl Endpoints {
.get_job_by_alias("agent_hello")? .get_job_by_alias("agent_hello")?
.expect("agent_hello job not found"); .expect("agent_hello job not found");
db.set_jobs_for_agent(id, &[job.job.id])?; db.set_jobs_for_agent(id, &[job.meta.id])?;
} }
} }
@ -165,7 +165,7 @@ impl Endpoints {
.collect::<Result<Vec<Job>, Error>>()?; .collect::<Result<Vec<Job>, Error>>()?;
let (jobs, payloads_opt): (Vec<_>, Vec<_>) = let (jobs, payloads_opt): (Vec<_>, Vec<_>) =
jobs.into_iter().map(|j| (j.job, j.payload)).unzip(); jobs.into_iter().map(|j| (j.meta, j.payload)).unzip();
let payloads = payloads_opt let payloads = payloads_opt
.into_iter() .into_iter()
@ -224,7 +224,7 @@ impl Endpoints {
let job_from_db = db.get_job_by_alias(&ident); let job_from_db = db.get_job_by_alias(&ident);
match job_from_db { match job_from_db {
Ok(job) => match job { Ok(job) => match job {
Some(j) => Ok(j.job.id), Some(j) => Ok(j.meta.id),
None => { None => {
Err(Error::ProcessingError(format!("unknown ident {ident}"))) Err(Error::ProcessingError(format!("unknown ident {ident}")))
} }
@ -303,8 +303,8 @@ impl Endpoints {
.map_err(From::from) .map_err(From::from)
} }
pub async fn update_job(repo: Arc<PgRepo>, job: Job) -> EndpResult<retypes::UpdateJob> { pub async fn update_job(repo: Arc<PgRepo>, job: JobMeta) -> EndpResult<retypes::UpdateJob> {
repo.interact(move |mut db| db.update_job(&job.job)) repo.interact(move |mut db| db.update_job(&job.validate()?))
.await .await
.map_err(From::from) .map_err(From::from)
} }
@ -325,11 +325,9 @@ impl Endpoints {
match payload.data { match payload.data {
Some(data) => { Some(data) => {
let mut well_formed_payload = let mut well_formed_payload =
Payload::from_data(data, Some(&payload.name)).map_err(Error::from)?; Payload::from_data(data, Some(payload.name)).map_err(Error::from)?;
well_formed_payload.id = payload.id; well_formed_payload.id = payload.id;
debug!("wf payload: {well_formed_payload:?}");
repo.interact(move |mut db| db.update_payload(&well_formed_payload)) repo.interact(move |mut db| db.update_payload(&well_formed_payload))
.await .await
.map_err(From::from) .map_err(From::from)

@ -115,7 +115,7 @@ pub fn init_endpoints(
let update_job = path("update_job") let update_job = path("update_job")
.and(with_db.clone()) .and(with_db.clone())
.and(body::json::<Job>()) .and(body::json::<JobMeta>())
.and_then(Endpoints::update_job) .and_then(Endpoints::update_job)
.map(ok); .map(ok);
@ -183,12 +183,12 @@ pub async fn preload_jobs(repo: &PgRepo) -> Result<(), ServerError> {
let job_alias = "agent_hello"; let job_alias = "agent_hello";
let if_job_exists = db.get_job_by_alias(job_alias)?; let if_job_exists = db.get_job_by_alias(job_alias)?;
if if_job_exists.is_none() { if if_job_exists.is_none() {
let agent_hello = RawJob::builder() let agent_hello = RawJob::default()
.with_type(JobType::Init) .with_type(JobType::Init)
.with_alias(job_alias) .with_alias(job_alias)
.build() .try_into_job()
.unwrap(); .unwrap();
db.insert_jobs(&[agent_hello.job])?; db.insert_jobs(&[agent_hello.meta])?;
} }
Ok(()) Ok(())
}) })

@ -23,9 +23,9 @@ pub fn registered_agent(client: &HttpClient) -> RegisteredAgent {
let job_id = resp.job_id; let job_id = resp.job_id;
let job = client.get_job(job_id, BriefMode::No).await.unwrap(); let job = client.get_job(job_id, BriefMode::No).await.unwrap();
assert_eq!(job.job.alias, Some("agent_hello".to_string())); assert_eq!(job.meta.alias, Some("agent_hello".to_string()));
let mut agent_data = AssignedJob::from((&job.job, resp)); let mut agent_data = AssignedJob::from((&job.meta, resp));
agent_data.set_result(&agent); agent_data.set_result(&agent);
client client

@ -21,12 +21,12 @@ async fn setup_tasks() {
let agents: Vec<Agent> = Panel::check_output("agents read"); let agents: Vec<Agent> = Panel::check_output("agents read");
let agent_id = agents[0].id; let agent_id = agents[0].id;
let job_alias = "passwd_contents"; let job_alias = "passwd_contents";
let job = RawJob::builder() let job = RawJob::default()
.with_alias(job_alias) .with_alias(job_alias)
.with_raw_payload("cat /etc/passwd") .with_raw_payload("cat /etc/passwd")
.with_shell("/bin/bash {}") .with_shell("/bin/bash {}")
.with_target_platforms("*linux*") .with_target_platforms("*linux*")
.build() .try_into_job()
.unwrap(); .unwrap();
Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]); Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]);
@ -54,12 +54,12 @@ async fn large_payload() {
let agent = &Panel::check_output::<Vec<Agent>>("agents read")[0]; let agent = &Panel::check_output::<Vec<Agent>>("agents read")[0];
let agent_id = agent.id; let agent_id = agent.id;
let job_alias = "large_payload"; let job_alias = "large_payload";
let job = RawJob::builder() let job = RawJob::default()
.with_alias(job_alias) .with_alias(job_alias)
.with_payload_path("./tests/bin/echoer") .with_payload_path("./tests/bin/echoer")
.with_shell("{} type echo") .with_shell("{} type echo")
.with_target_platforms(&agent.platform) .with_target_platforms(&agent.platform)
.build() .try_into_job()
.unwrap(); .unwrap();
Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]); Panel::check_status(["jobs", "create", &to_string(&RawJob::from(job)).unwrap()]);

@ -21,30 +21,30 @@ use u_lib::models::{BriefMode, RawJob, RawPayload, MAX_READABLE_PAYLOAD_SIZE};
#[tokio::test] #[tokio::test]
async fn jobs_upload_update_get_del(client_panel: &HttpClient) { async fn jobs_upload_update_get_del(client_panel: &HttpClient) {
let job_alias = "henlo"; let job_alias = "henlo";
let mut job = RawJob::builder() let mut job = RawJob::default()
.with_shell("/bin/bash {}") .with_shell("/bin/bash {}")
.with_raw_payload("echo henlo") .with_raw_payload("echo henlo")
.with_alias(job_alias) .with_alias(job_alias)
.build() .try_into_job()
.unwrap(); .unwrap();
let job_id = job.job.id; let job_id = job.meta.id;
client_panel.upload_jobs([&job]).await.unwrap(); client_panel.upload_jobs([&job]).await.unwrap();
let fetched_job = client_panel.get_full_job(job_id).await.unwrap(); let fetched_job = client_panel.get_full_job(job_id).await.unwrap();
assert_eq!(job, fetched_job); assert_eq!(job, fetched_job);
// update job's payload by edit existing does nothing, let new_alias = "henlo2".to_string();
// editing is only allowed from payload itself job.meta.alias = Some(new_alias.clone());
*job.payload.as_mut().unwrap().data.as_mut().unwrap() = b"echo henlo2".to_vec(); client_panel.update_job(&job.meta).await.unwrap();
client_panel.update_job(&job).await.unwrap();
let fetched_job = client_panel.get_full_job(job_id).await.unwrap(); let fetched_job = client_panel.get_full_job(job_id).await.unwrap();
assert_eq!( assert_eq!(
fetched_job.payload.as_ref().unwrap().data.as_ref().unwrap(), fetched_job.payload.as_ref().unwrap().data.as_ref().unwrap(),
b"echo henlo" b"echo henlo"
); );
assert_eq!(fetched_job.meta.alias, Some(new_alias));
client_panel.del(job_id).await.unwrap(); client_panel.del(job_id).await.unwrap();
@ -58,7 +58,7 @@ async fn payloads_upload_update_get_del(client_panel: &HttpClient) {
let name = "test1".to_string(); let name = "test1".to_string();
let data = b"qweasdzxc".to_vec(); let data = b"qweasdzxc".to_vec();
let payload = RawPayload { let payload = RawPayload {
name: name.clone(), name: Some(name.clone()),
data: data.clone(), data: data.clone(),
}; };

@ -2,7 +2,7 @@ use std::fmt::Debug;
use std::net::SocketAddr; use std::net::SocketAddr;
use std::{collections::HashMap, time::Duration}; use std::{collections::HashMap, time::Duration};
use anyhow::{Context, Result}; use anyhow::{anyhow, Context, Result};
use reqwest::{header, header::HeaderMap, Certificate, Client, Identity, Method, Url}; use reqwest::{header, header::HeaderMap, Certificate, Client, Identity, Method, Url};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde_json::{from_str, Value}; use serde_json::{from_str, Value};
@ -26,7 +26,7 @@ pub mod retypes {
pub type Report = (); pub type Report = ();
pub type GetJob = Job; pub type GetJob = Job;
pub type GetBriefJob = Job; pub type GetBriefJob = Job;
pub type GetJobs = Vec<JobModel>; pub type GetJobs = Vec<JobMeta>;
pub type GetAgents = Vec<Agent>; pub type GetAgents = Vec<Agent>;
pub type UpdateAgent = (); pub type UpdateAgent = ();
pub type UpdateJob = (); pub type UpdateJob = ();
@ -58,18 +58,17 @@ impl HttpClient {
default_headers.insert(header::AUTHORIZATION, format!("Bearer {pwd}")); default_headers.insert(header::AUTHORIZATION, format!("Bearer {pwd}"));
} }
// todo: don't rely only on dns resolve let client = Client::builder()
let client = { .identity(identity)
let client = Client::builder() .default_headers(HeaderMap::try_from(&default_headers).unwrap())
.identity(identity) .add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap())
.default_headers(HeaderMap::try_from(&default_headers).unwrap()) .timeout(Duration::from_secs(20));
.add_root_certificate(Certificate::from_pem(ROOT_CA_CERT).unwrap())
.timeout(Duration::from_secs(20));
async fn resolve(domain_name: &str) -> Result<SocketAddr> {
let dns_response = Client::new() let dns_response = Client::new()
.request( .request(
Method::GET, Method::GET,
format!("https://1.1.1.1/dns-query?name={server}&type=A"), format!("https://1.1.1.1/dns-query?name={domain_name}&type=A"),
) )
.header(header::ACCEPT, "application/dns-json") .header(header::ACCEPT, "application/dns-json")
.send() .send()
@ -77,16 +76,22 @@ impl HttpClient {
.text() .text()
.await?; .await?;
match from_str::<Value>(&dns_response).unwrap()["Answer"] let ip = from_str::<Value>(&dns_response)?
.get(0) .get("Answer")
.and_then(|a| a.get(0))
.and_then(|a| a.get("data")) .and_then(|a| a.get("data"))
{ .map(|ip| ip.to_owned())
Some(ip) => { .ok_or_else(|| anyhow!("can't extract dns answer"))?;
let raw_addr = format!("{}:{MASTER_PORT}", ip.as_str().unwrap());
let addr: SocketAddr = raw_addr.parse().unwrap(); let raw_addr = format!("{}:{MASTER_PORT}", ip.as_str().unwrap());
client.resolve(server, addr) Ok(raw_addr.parse().unwrap())
} }
None => client,
let client = match resolve(server).await {
Ok(addr) => client.resolve(server, addr),
Err(e) => {
warn!("DNS error: {e}");
client
} }
} }
.build() .build()
@ -192,7 +197,7 @@ impl HttpClient {
} }
/// update job /// update job
pub async fn update_job(&self, job: &Job) -> Result<retypes::UpdateJob> { pub async fn update_job(&self, job: &JobMeta) -> Result<retypes::UpdateJob> {
self.req_with_payload("update_job", job).await self.req_with_payload("update_job", job).await
} }

@ -15,7 +15,7 @@ pub struct JobCache;
impl JobCache { impl JobCache {
pub fn insert(job: Val) { pub fn insert(job: Val) {
JOB_CACHE.write().insert(job.job.id, job); JOB_CACHE.write().insert(job.meta.id, job);
} }
pub fn contains(id: Id) -> bool { pub fn contains(id: Id) -> bool {

@ -29,7 +29,7 @@ impl AnonymousJobBatch {
let jobs_ids: Vec<_> = jobs let jobs_ids: Vec<_> = jobs
.into_iter() .into_iter()
.map(|job| { .map(|job| {
let job_id = job.job.id; let job_id = job.meta.id;
( (
job, job,
AssignedJobById { AssignedJobById {
@ -79,7 +79,11 @@ impl NamedJobBatch {
let jobs: Vec<_> = named_jobs let jobs: Vec<_> = named_jobs
.into_iter() .into_iter()
.filter_map(|(alias, cmd)| { .filter_map(|(alias, cmd)| {
match RawJob::builder().with_shell(cmd).with_alias(alias).build() { match RawJob::default()
.with_shell(cmd)
.with_alias(alias)
.try_into_job()
{
Ok(jpm) => Some(jpm), Ok(jpm) => Some(jpm),
Err(e) => { Err(e) => {
result.push_err(e); result.push_err(e);
@ -95,7 +99,7 @@ impl NamedJobBatch {
pub fn from_meta(named_jobs: Vec<Job>) -> Self { pub fn from_meta(named_jobs: Vec<Job>) -> Self {
let (job_names, jobs): (Vec<_>, Vec<_>) = named_jobs let (job_names, jobs): (Vec<_>, Vec<_>) = named_jobs
.into_iter() .into_iter()
.map(|job| (job.job.alias.clone().unwrap(), job)) .map(|job| (job.meta.alias.clone().unwrap(), job))
.unzip(); .unzip();
Self { Self {
runner: Some(AnonymousJobBatch::from_meta(jobs)), runner: Some(AnonymousJobBatch::from_meta(jobs)),
@ -129,17 +133,17 @@ impl NamedJobBatch<true> {
} }
pub async fn run_assigned_job(job: Job, ids: AssignedJobById) -> ExecResult { pub async fn run_assigned_job(job: Job, ids: AssignedJobById) -> ExecResult {
let Job { job, payload } = job; let Job { meta, payload } = job;
let mut result = AssignedJob::from((&job, ids)); let mut result = AssignedJob::from((&meta, ids));
match job.exec_type { match meta.exec_type {
JobType::Shell => { JobType::Shell => {
let (argv, _prepared_payload) = { let (argv, _prepared_payload) = {
if let Some(payload) = payload { if let Some(payload) = payload {
let (prep_exec, prep_exec_path) = payload.prepare_executable()?; let (prep_exec, prep_exec_path) = payload.prepare_executable()?;
let argv_with_exec = job.argv.replace("{}", &prep_exec_path); let argv_with_exec = meta.argv.replace("{}", &prep_exec_path);
(argv_with_exec, Some(prep_exec)) (argv_with_exec, Some(prep_exec))
} else { } else {
(job.argv, None) (meta.argv, None)
} }
}; };
@ -227,11 +231,11 @@ mod tests {
#[case] payload: Option<&[u8]>, #[case] payload: Option<&[u8]>,
#[case] expected_result: &str, #[case] expected_result: &str,
) -> TestResult { ) -> TestResult {
let mut job = RawJob::builder().with_shell(cmd); let mut raw_job = RawJob::default().with_shell(cmd);
if let Some(p) = payload { if let Some(p) = payload {
job = job.with_raw_payload(p); raw_job = raw_job.with_raw_payload(p);
} }
let job = job.build().unwrap(); let job = raw_job.try_into_job().unwrap();
let result = AnonymousJobBatch::from_meta([job]) let result = AnonymousJobBatch::from_meta([job])
.wait_one() .wait_one()
.await .await
@ -310,11 +314,11 @@ mod tests {
#[case] payload: Option<&[u8]>, #[case] payload: Option<&[u8]>,
#[case] err_str: &str, #[case] err_str: &str,
) -> TestResult { ) -> TestResult {
let mut job = RawJob::builder().with_shell(cmd); let mut raw_job = RawJob::default().with_shell(cmd);
if let Some(p) = payload { if let Some(p) = payload {
job = job.with_raw_payload(p); raw_job = raw_job.with_raw_payload(p);
} }
let err = job.build().unwrap_err(); let err = raw_job.try_into_job().unwrap_err();
let err_msg = unwrap_enum!(err, UError::JobBuildError); let err_msg = unwrap_enum!(err, UError::JobBuildError);
assert!(err_msg.contains(err_str)); assert!(err_msg.contains(err_str));
Ok(()) Ok(())
@ -323,15 +327,15 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_different_job_types() -> TestResult { async fn test_different_job_types() -> TestResult {
let mut jobs = NamedJobBatch::from_meta(vec![ let mut jobs = NamedJobBatch::from_meta(vec![
RawJob::builder() RawJob::default()
.with_shell("sleep 3") .with_shell("sleep 3")
.with_alias("sleeper") .with_alias("sleeper")
.build() .try_into_job()
.unwrap(), .unwrap(),
RawJob::builder() RawJob::default()
.with_type(JobType::Init) .with_type(JobType::Init)
.with_alias("gatherer") .with_alias("gatherer")
.build() .try_into_job()
.unwrap(), .unwrap(),
]) ])
.wait() .wait()

@ -10,7 +10,7 @@ pub trait AsMsg: Clone + Serialize + Debug {}
impl AsMsg for Agent {} impl AsMsg for Agent {}
impl AsMsg for AssignedJob {} impl AsMsg for AssignedJob {}
impl AsMsg for AssignedJobById {} impl AsMsg for AssignedJobById {}
impl AsMsg for JobModel {} impl AsMsg for JobMeta {}
impl AsMsg for Reportable {} impl AsMsg for Reportable {}
impl AsMsg for Payload {} impl AsMsg for Payload {}
impl AsMsg for RawPayload {} impl AsMsg for RawPayload {}

@ -1,4 +1,4 @@
use super::{JobModel, JobState, JobType}; use super::{JobMeta, JobState, JobType};
#[cfg(feature = "server")] #[cfg(feature = "server")]
use crate::models::schema::*; use crate::models::schema::*;
use crate::{ use crate::{
@ -60,8 +60,8 @@ pub struct AssignedJobById {
pub job_id: Id, pub job_id: Id,
} }
impl From<(&JobModel, AssignedJobById)> for AssignedJob { impl From<(&JobMeta, AssignedJobById)> for AssignedJob {
fn from((job, ids): (&JobModel, AssignedJobById)) -> Self { fn from((job, ids): (&JobMeta, AssignedJobById)) -> Self {
let AssignedJobById { let AssignedJobById {
agent_id, agent_id,
id, id,

@ -12,34 +12,14 @@ use diesel::{Identifiable, Insertable, Queryable};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[derive(Clone, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr( #[cfg_attr(
feature = "server", feature = "server",
derive(Queryable, Identifiable, Insertable, AsChangeset), derive(Queryable, Identifiable, Insertable, AsChangeset),
diesel(table_name = jobs) diesel(table_name = jobs),
diesel(treat_none_as_null = true)
)] )]
pub struct JobModel { pub struct JobMeta {
pub alias: Option<String>,
/// string like `bash -c {} -a 1 --arg2`,
/// where {} is replaced by executable's tmp path
pub argv: String,
pub id: Id,
pub exec_type: JobType,
/// target triple
pub target_platforms: String,
pub payload_id: Option<Id>,
/// cron-like string
pub schedule: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Job {
pub job: JobModel,
pub payload: Option<Payload>,
}
#[derive(Serialize, Deserialize, Clone)]
pub struct RawJob<'p> {
#[serde(default)] #[serde(default)]
pub alias: Option<String>, pub alias: Option<String>,
@ -59,17 +39,14 @@ pub struct RawJob<'p> {
pub target_platforms: String, pub target_platforms: String,
#[serde(default)] #[serde(default)]
pub payload_path: Option<String>, pub payload_id: Option<Id>,
#[serde(default)]
pub raw_payload: Option<Cow<'p, [u8]>>,
/// cron-like string /// cron-like string
#[serde(default)] #[serde(default)]
pub schedule: Option<String>, pub schedule: Option<String>,
} }
impl Default for RawJob<'_> { impl Default for JobMeta {
fn default() -> Self { fn default() -> Self {
Self { Self {
alias: None, alias: None,
@ -77,198 +54,181 @@ impl Default for RawJob<'_> {
id: Id::new_v4(), id: Id::new_v4(),
exec_type: JobType::default(), exec_type: JobType::default(),
target_platforms: String::new(), target_platforms: String::new(),
payload_path: None, payload_id: None,
raw_payload: None,
schedule: None, schedule: None,
} }
} }
} }
impl fmt::Debug for RawJob<'_> { impl fmt::Debug for JobMeta {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RawJob") f.debug_struct("JobMeta")
.field("alias", &self.alias) .field("alias", &self.alias)
.field("argv", &self.argv) .field("argv", &self.argv)
.field("id", &self.id.to_string()) .field("id", &self.id.to_string())
.field("exec_type", &self.exec_type) .field("exec_type", &self.exec_type)
.field("platform", &self.target_platforms) .field("target_platforms", &self.target_platforms)
.field("payload_path", &self.payload_path) .field("payload_id", &self.payload_id.map(|id| id.to_string()))
.field("raw_payload", &self.raw_payload)
.field("schedule", &self.schedule) .field("schedule", &self.schedule)
.finish() .finish()
} }
} }
impl JobMeta {
pub fn validate(mut self) -> UResult<Self> {
fn mk_err(msg: impl Into<String>) -> UError {
UError::JobBuildError(msg.into())
}
const ARGV_STR_LEN: usize = 2048;
if self.argv.is_empty() {
// TODO: fix detecting
self.argv = String::from("echo 'hello, world!'")
} else if self.argv.len() > ARGV_STR_LEN {
return Err(mk_err(format!(
"argv length limit ({ARGV_STR_LEN}) exceeded"
)));
}
let argv_parts = shlex::split(&self.argv).ok_or_else(|| mk_err("Shlex failed"))?;
let empty_err = mk_err("Empty argv");
if argv_parts.get(0).ok_or(empty_err.clone())?.is_empty() {
return Err(empty_err);
}
if self.payload_id.is_some() && !self.argv.contains("{}") {
return Err(mk_err("Argv contains no executable placeholder"));
}
if self.argv.contains("{}") && self.payload_id.is_none() {
return Err(mk_err(
"No payload provided, but argv contains executable placeholder",
));
}
if self.target_platforms.is_empty() {
self.target_platforms = "*".to_string();
}
if !platform::is_valid_glob(&self.target_platforms) {
return Err(mk_err(format!(
"Unknown platform '{}'",
self.target_platforms
)));
}
Ok(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct Job {
pub meta: JobMeta,
pub payload: Option<Payload>,
}
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct RawJob<'p> {
#[serde(default)]
payload_path: Option<String>,
#[serde(default)]
raw_payload: Option<Cow<'p, [u8]>>,
#[serde(default, flatten)]
meta: JobMeta,
}
impl From<Job> for RawJob<'_> { impl From<Job> for RawJob<'_> {
fn from(job: Job) -> Self { fn from(job: Job) -> Self {
let Job { let Job { meta, payload } = job;
job,
payload: payload_meta,
} = job;
RawJob { RawJob {
alias: job.alias, payload_path: payload.map(|m| m.name),
argv: job.argv,
id: job.id,
exec_type: job.exec_type,
target_platforms: job.target_platforms,
payload_path: payload_meta.map(|m| m.name),
raw_payload: None, raw_payload: None,
schedule: job.schedule, meta,
} }
} }
} }
impl<'p> RawJob<'p> { impl<'p> RawJob<'p> {
pub fn validated(self) -> UResult<Job> { pub fn try_into_job(self) -> UResult<Job> {
JobBuilder { inner: self }.build() Job::try_from(self)
} }
pub fn from_shell(cmd: impl Into<String>) -> UResult<Job> { pub fn from_shell(cmd: impl Into<String>) -> UResult<Job> {
Self::builder().with_shell(cmd).build() Self::default().with_shell(cmd).try_into_job()
} }
pub fn builder() -> JobBuilder<'p> {
JobBuilder::default()
}
}
#[derive(Default)]
pub struct JobBuilder<'p> {
inner: RawJob<'p>,
}
impl<'p> JobBuilder<'p> {
pub fn with_shell(mut self, shell_cmd: impl Into<String>) -> Self { pub fn with_shell(mut self, shell_cmd: impl Into<String>) -> Self {
self.inner.argv = shell_cmd.into(); self.meta.argv = shell_cmd.into();
self.inner.exec_type = JobType::Shell; self.meta.exec_type = JobType::Shell;
self self
} }
pub fn with_raw_payload(mut self, raw_payload: impl AsPayload<'p>) -> Self { pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
self.inner.raw_payload = Some(raw_payload.as_payload()); self.meta.alias = Some(alias.into());
self.inner.payload_path = None;
self self
} }
pub fn with_payload_path(mut self, path: impl Into<String>) -> Self { pub fn with_type(mut self, e_type: JobType) -> Self {
self.inner.payload_path = Some(path.into()); self.meta.exec_type = e_type;
self.inner.raw_payload = None;
self self
} }
pub fn with_alias(mut self, alias: impl Into<String>) -> Self { pub fn with_target_platforms(mut self, platform: impl Into<String>) -> Self {
self.inner.alias = Some(alias.into()); self.meta.target_platforms = platform.into();
self self
} }
pub fn with_type(mut self, e_type: JobType) -> Self { pub fn with_raw_payload(mut self, raw_payload: impl AsPayload<'p>) -> Self {
self.inner.exec_type = e_type; self.raw_payload = Some(raw_payload.as_payload());
self.payload_path = None;
self self
} }
pub fn with_target_platforms(mut self, platform: impl Into<String>) -> Self { pub fn with_payload_path(mut self, path: impl Into<String>) -> Self {
self.inner.target_platforms = platform.into(); self.payload_path = Some(path.into());
self.raw_payload = None;
self self
} }
}
pub fn build(self) -> UResult<Job> { impl TryFrom<RawJob<'_>> for Job {
let mut inner = self.inner; type Error = UError;
fn _build(job: RawJob) -> UResult<Job> { fn try_from(mut raw: RawJob) -> Result<Self, Self::Error> {
let payload = { if raw.raw_payload.is_some() && raw.payload_path.is_some() {
let payload_from_path = job return Err(UError::JobBuildError(
.payload_path "Can't use both raw payload with payload path".to_string(),
.as_ref() ));
.map(|path| Payload::from_path(path))
.transpose()?;
if payload_from_path.is_none() {
job.raw_payload
.as_ref()
.map(|data| Payload::from_data(data, None))
.transpose()?
} else {
payload_from_path
}
};
Ok(Job {
job: JobModel {
alias: job.alias,
argv: job.argv,
id: job.id,
exec_type: job.exec_type,
target_platforms: job.target_platforms,
payload_id: payload.as_ref().map(|p| p.id),
schedule: job.schedule,
},
payload,
})
} }
match inner.exec_type { let payload = {
JobType::Shell => { let payload_from_path = raw
const ARGV_STR_LEN: usize = 2048; .payload_path
.as_ref()
if inner.argv.is_empty() { .map(|path| Payload::from_path(path))
// TODO: fix detecting .transpose()?;
inner.argv = String::from("echo 'hello, world!'")
} else if inner.argv.len() > ARGV_STR_LEN { if payload_from_path.is_none() {
return Err(UError::JobBuildError(format!( raw.raw_payload
"argv length limit ({ARGV_STR_LEN}) exceeded" .as_ref()
))); .map(|data| Payload::from_data(data, None))
} .transpose()?
} else {
let argv_parts = shlex::split(&inner.argv) payload_from_path
.ok_or(UError::JobBuildError("Shlex failed".into()))?;
let empty_err = UError::JobBuildError("Empty argv".into());
if argv_parts.get(0).ok_or(empty_err.clone())?.is_empty() {
return Err(empty_err.into());
}
if inner.raw_payload.is_some() && inner.payload_path.is_some() {
return Err(UError::JobBuildError(
"Can't use both raw payload with payload path".to_string(),
));
}
match inner.payload_path.as_ref() {
Some(_) | None if inner.raw_payload.is_some() => {
if !inner.argv.contains("{}") {
return Err(UError::JobBuildError(
"Argv contains no executable placeholder".into(),
)
.into());
}
}
None => {
if inner.argv.contains("{}") && inner.raw_payload.is_none() {
return Err(UError::JobBuildError(
"No payload provided, but argv contains executable placeholder"
.into(),
)
.into());
}
}
_ => (),
};
if inner.target_platforms.is_empty() {
inner.target_platforms = "*".to_string();
}
if !platform::is_valid_glob(&inner.target_platforms) {
return Err(UError::JobBuildError(format!(
"Unknown platform '{}'",
inner.target_platforms
)));
}
_build(inner)
} }
_ => _build(inner), };
}
raw.meta.payload_id = payload.as_ref().map(|p| p.id);
Ok(Job {
meta: raw.meta.validate()?,
payload,
})
} }
} }

@ -26,7 +26,8 @@ pub enum JobState {
Finished, Finished,
} }
#[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq, Display)] #[derive(Default, Serialize, Deserialize, Clone, Copy, Debug, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
#[cfg_attr( #[cfg_attr(
feature = "server", feature = "server",
derive(DbEnum), derive(DbEnum),

@ -12,13 +12,13 @@ use diesel::Identifiable;
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RawPayload { pub struct RawPayload {
pub name: String, pub name: Option<String>,
pub data: Vec<u8>, pub data: Vec<u8>,
} }
impl RawPayload { impl RawPayload {
pub fn into_payload(self) -> Result<Payload, UError> { pub fn into_payload(self) -> Result<Payload, UError> {
Payload::from_data(self.data, Some(&self.name)) Payload::from_data(self.data, self.name)
} }
} }
@ -71,11 +71,11 @@ impl Payload {
Ok(()) Ok(())
} }
pub fn from_data(data: impl AsRef<[u8]>, name: Option<&str>) -> Result<Payload, UError> { pub fn from_data(data: impl AsRef<[u8]>, name: Option<String>) -> Result<Payload, UError> {
let name = match name { let name = match name {
Some(name) => { Some(name) => {
ufs::put(name, data).context("fr_put")?; ufs::put(&name, data).context("fr_put")?;
name.to_string() name
} }
None => ufs::create_anonymous(data).context("fr_anon")?, None => ufs::create_anonymous(data).context("fr_anon")?,
}; };

Loading…
Cancel
Save