Skip to content
Snippets Groups Projects
Commit 2a260a4f authored by Chris Hines's avatar Chris Hines
Browse files

rework the way the SPA specifies apps, identities and batch scheduler interfaces

parent a11489a2
No related branches found
No related tags found
No related merge requests found
Showing
with 214 additions and 88 deletions
......@@ -7,7 +7,9 @@ import { TokenextractorComponent } from './tokenextractor/tokenextractor.compone
const routes: Routes = [
{ path: '', redirectTo: 'launch', pathMatch: 'full'},
{ path: 'launch', component: LauncherComponent},
{ path: 'sshauthz_callback', component: TokenextractorComponent}
// { path: 'sshauthz_callback', component: TokenextractorComponent}
{ path: 'sshauthz_callback', component: LauncherComponent}
];
......
......@@ -3,7 +3,7 @@
<span>{{title}}</span>
<span class="fill-remaining-space"></span>
<span class="fill-remaining-space"></span>
<app-siteselection></app-siteselection>
<!-- <app-siteselection></app-siteselection> -->
<button mat-button *ngIf="authorised" (click)="logout()">Logout</button>
<button mat-button *ngIf="!authorised" (click)="login()">Login</button>
</mat-toolbar-row>
......
......@@ -10,7 +10,7 @@ export class AppComponent {
title = 'Strudelv2';
private loggedin: Boolean = false;
private errmsg: string='';
private authorised: boolean;
public authorised: boolean;
// private testingAuth: boolean;
constructor(private tesService: TesService,) {
......
export class BatchInterface {
cancelcmd: string;
submitcmd: string;
statcmd: string;
internalfirewall: boolean;
}
export class Computesite {
url: string;
name: string;
url: string; // The URL runs a web service to help construct sbatch commands
// Infact the URL should return the entire interface for
// submit stat and cancel
name: string; // Human readable name
host: string; // Login host
cafingerprint: string; // Certificates contain a CA fingerprint. We use this
// to figure out which compute site a certificate is valid
// for
}
import {Computesite} from './computesite';
// Identities are defined by a username on a computer, but rather than just
// DNS entry, there is extra info in the Computesite
export class Identity {
username: string;
site: Computesite;
}
import {Computesite} from './computesite';
import {Identity} from './identity';
import {Strudelapp} from './strudelapp';
export class Job {
public name: string;
public jobid: string;
......@@ -5,4 +8,6 @@ export class Job {
public state: string;
public time: number;
public batch_host: string;
public identity: Identity;
public app: Strudelapp;
}
......@@ -11,7 +11,7 @@ import { StrudelappsService } from '../strudelapps.service';
export class JobComponent implements OnInit {
@Input() jobdata: Job;
private available: Boolean;
public available: Boolean;
private busy: Boolean;
constructor(private tesService: TesService, private strudelAppsService: StrudelappsService) {
......@@ -28,15 +28,14 @@ export class JobComponent implements OnInit {
}
onCancel() {
this.tesService.cancel(this.jobdata.jobid);
this.tesService.cancel(this.jobdata);
}
onConnect() {
console.log('attempting connect');
// Before connecting we must resolve what type of app we are connecting to
let app = this.strudelAppsService.getApp(this.jobdata.name);
console.log('app definition is ',app);
this.tesService.connect(this.jobdata.jobid,this.jobdata.batch_host,app);
this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name);
this.tesService.connect(this.jobdata);
}
}
<div *ngIf="jobs.lenght == 0">
<div *ngIf="jobs.length == 0">
<mat-card>
You don't appear to have any jobs
</mat-card>
</div>
<div *ngFor="let job of jobs">
<app-job [jobdata]=job></app-job>
......
......@@ -11,7 +11,7 @@ import { Observable } from 'rxjs/Observable';
styleUrls: ['./joblist.component.css']
})
export class JoblistComponent implements OnInit {
private jobs: any[] = [];
public jobs: any[] = [];
private displayedColumns = ['id'];
private jobsSubscription: any;
......
......@@ -3,6 +3,7 @@ import {Strudelapp} from '../strudelapp';
import { StrudelappsService } from '../strudelapps.service';
import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material';
import { TesService } from '../tes.service';
import {Identity } from '../identity';
......@@ -15,7 +16,8 @@ export class LauncherComponent implements OnInit {
private strudelapps: Strudelapp[];
private app: Strudelapp;
private authorised: boolean;
public authorised: boolean;
private identity: Identity;
constructor(private strudelappsService: StrudelappsService, public dialog: MatDialog, private tesService: TesService) { }
......@@ -36,7 +38,8 @@ export class LauncherComponent implements OnInit {
submitApp() {
console.log('submitting app for',this.app.name);
this.tesService.submit(this.app);
this.identity = this.tesService.identities[0];
this.tesService.submit(this.app, this.identity);
// this.openDialog('This is where we should start the app');
}
......
import { Computesite } from './computesite';
export const COMPUTESITES: Computesite[] = [
{ url: 'http://localhost:8080/config', name: 'M3' },
{url: '', name: 'Other' },
{ url: 'http://localhost:8080/config', name: 'M3',
host: 'm3.massive.org.au',
cafingerprint: 'RSA SHA256:cmDxHrZQSPlBMUUcI/BWmruXho1XOzfXPDHSqVTwV2I' },
];
......@@ -8,8 +8,8 @@ import { ComputesitesService} from '../computesites.service';
styleUrls: ['./siteselection.component.css']
})
export class SiteselectionComponent implements OnInit {
private computesites: Computesite[];
private siteurl: String;
public computesites: Computesite[];
public siteurl: String;
constructor(private computesiteSerivce: ComputesitesService) { }
ngOnInit() {
......
export class Strudelapp {
url: string;
name: string;
startscript: string;
paramscmd: string;
url: string; // A url used to retrieve extra config options. May be null
name: string; // Human readable name
startscript: string; // batch script ... should NOT include resource directives
// resource directives like #SBATCH belong in the batchinterface
paramscmd: string; // command to return extra data such as passwords and tokens
// values returned here can be used in the client strings
client: {cmd: string[], redir: string};
localbind: boolean;
localbind: boolean; // does the application bind to a port on the localhost
// interface or on all interfaces. This behaviour determins
/// how we create tunnels
}
......@@ -6,11 +6,19 @@ import { catchError, map, tap } from 'rxjs/operators';
import { Job } from './job';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import { Strudelapp } from './strudelapp';
import { Computesite } from './computesite';
import { Identity } from './identity';
import { BatchInterface} from './batchinterface';
import {ComputesitesService} from './computesites.service';
import { StrudelappsService } from './strudelapps.service';
import { timer } from 'rxjs/observable/timer';
import { repeat } from 'rxjs/operators';
import {LocationStrategy} from '@angular/common';
// import { keypair } from 'keypair';
import * as keypair from 'keypair';
import * as forge from "node-forge";
import { ActivatedRoute } from '@angular/router';
// import keypair = require('keypair');
// import forge = require('node-forge');
......@@ -18,9 +26,10 @@ import * as forge from "node-forge";
@Injectable()
export class TesService {
private Base='http://localhost:5000'
//private Base='https://vm-118-138-240-255.erc.monash.edu.au/tes'
public authorised: BehaviorSubject<boolean>;
public tesSelected: BehaviorSubject<boolean>;
public errMsg: BehaviorSubject<any>;
public statusMsg: BehaviorSubject<any>;
public jobs: any[];
public busy: BehaviorSubject<boolean> ;
public testingAuth: BehaviorSubject<boolean>;
......@@ -34,11 +43,17 @@ private client_id: string;
private keypair: any;
private ssh: any;
private sshcert: any;
private identities: Identity[];
private batchinterface: BatchInterface;
constructor(private http: HttpClient) {
this.errMsg = new BehaviorSubject<any>('');
constructor(private http: HttpClient,
private locationStrategy: LocationStrategy,
private route: ActivatedRoute,
private computesite: ComputesitesService,
private strudelappsService: StrudelappsService) {
this.statusMsg = new BehaviorSubject<any>('');
this.authorised = new BehaviorSubject<boolean>(false);
this.tesSelected = new BehaviorSubject<boolean>(true);
this.testingAuth = new BehaviorSubject<boolean>(false);
......@@ -50,57 +65,101 @@ private sshcert: any;
this.sshauthz = this.sshauthzbase + 'oauth/authorize';
this.signing_endpoint = this.sshauthzbase + 'api/v1/sign_key';
this.client_id = "86c06039-e589-4b39-9d1f-9eca431be18f";
this.route.fragment.subscribe(frag => this.getCert(frag));
this.batchinterface = new BatchInterface();
this.identities=[];
this.batchinterface = { cancelcmd: 'scancel {jobid}', statcmd: '/home/chines/jsonstat.py',
submitcmd: 'sbatch --partition=m3f', internalfirewall: false }
}
updateJoblist(resp) {
let joblist = <Job[]>resp;
for (let j of joblist) {
j.app = this.strudelappsService.getApp(j.name)
}
this.joblist.next(joblist);
this.statusMsg.next(null);
}
submitted(resp: any ) {
this.busy.next(false);
this.statusMsg.next('Updating job list');
this.getJobs();
}
buildParams(app: Strudelapp, identity: Identity, batchinterface: BatchInterface): string {
let params = new URLSearchParams();
let appstr = JSON.stringify(app);
params.set('app',appstr);
let interfacestr = JSON.stringify(batchinterface);
params.set('interface',interfacestr);
let identitystr = JSON.stringify(identity);
params.set('identity',identitystr);
return params.toString();
}
getJobs() {
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.errMsg.next(null);
this.http.get<Job[]>(this.Base+'/stat',options)
this.statusMsg.next(null);
for (let identity of this.identities) {
let paramstr = this.buildParams(null,identity,this.batchinterface);
this.http.get<Job[]>(this.Base+'/stat'+'?'+paramstr,options)
.pipe(catchError(this.handleError('getJobs',[])))
.subscribe(resp => this.joblist.next(resp));
.subscribe(resp => this.updateJoblist(resp));
}
}
submit(app: Strudelapp) {
submit(app: Strudelapp, identity: Identity) {
console.log("In tes submit url "+this.Base+'/submit');
console.log("starting app"+app.name);
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.errMsg.next(null);
this.statusMsg.next('Submitting job');
this.busy.next(true);
this.http.post<any>(this.Base+'/appsetup',app, options)
.pipe(catchError(this.handleError('submit',[])))
.subscribe(resp => this.http.post<any>(this.Base+'/submit',{}, options)
.pipe(catchError(this.handleError('submit',[])))
.subscribe(resp => this.busy.next(false)));
let paramstr = this.buildParams(app,identity,this.batchinterface);
this.http.post<any>(this.Base+'/submit'+'?'+paramstr,{}, options)
.pipe(catchError(this.handleError('submit',[])))
.subscribe(resp => this.busy.next(false));
}
cancel(jobid: string) {
cancel(job: Job) {
console.log("In tes cancel");
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.errMsg.next(null);
this.statusMsg.next(null);
let data = {};
this.http.delete<any>(this.Base+'/cancel/'+jobid, options)
let paramstr = this.buildParams(job.app,job.identity,this.batchinterface);
this.http.delete<any>(this.Base+'/cancel/'+job.jobid+'?'+paramstr, options)
.pipe(catchError(this.handleError('cancel',[])))
.subscribe(resp => this.submitted(resp));
}
submitted(resp: any ) {
public connect(job: Job) {
this.statusMsg.next(null);
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.busy.next(true);
let paramstr = this.buildParams(job.app,job.identity,this.batchinterface);
let appwindow = window.open(this.Base+"/connect/"+job.jobid+"/"+job.batch_host+"?"+paramstr);
appwindow.focus();
this.busy.next(false);
}
getCert(frag: any) {
getCert(frag: string) {
// Given a URL fragment, extract the authentication token, and use it to get
// an SSH certificate. SSH cert to be used on subsequent calls to the TES
console.log('in tesservice getCert');
console.log('in tesservice getCert argument is',frag);
console.log('fragment.getvalue is')
console.log(frag)
let match = frag.match(/access_token\=([\S\s]*?)[&|$]/);
if (match == null) {
this.errMsg.next("No access token: It looks like your login to SSHAuthZ didn't work.");
this.testingAuth.next(false);
this.authorised.next(false);
return;
}
this.authorised.next(true);
console.log(match);
......@@ -111,9 +170,13 @@ private sshcert: any;
// let data = {'token': this.token, 'sshauthz_endpoint': this.signing_endpoint}
// console.log(data);
console.log("Generating key matter")
let starttime = new Date();
this.keypair = keypair();
let publicKey = forge.pki.publicKeyFromPem(this.keypair.public);
this.ssh = forge.ssh.publicKeyToOpenSSH(publicKey, 'sv2@monash.edu');
let endtime = new Date();
console.log("generating new keys took", endtime.valueOf() - starttime.valueOf())
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
......@@ -121,6 +184,7 @@ private sshcert: any;
let data = {'token': this.token, 'pubkey': this.ssh, 'signing_url': this.signing_endpoint};
this.busy.next(true);
this.statusMsg.next("Generating Certificates ...")
console.log('posting to getcert',this.Base);
this.http.post<any>(this.Base+'/getcert',data, options)
.pipe(catchError(this.handleError('getCert',[])))
......@@ -135,63 +199,92 @@ private storeCert(resp) {
console.log(resp['cert']);
this.sshcert = resp['cert'];
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.statusMsg.next("Authorising ...")
let data = {'key': this.keypair.private, 'cert': this.sshcert};
this.http.post<any>(this.Base+'/sshagent',data,options)
.pipe(catchError(this.handleError('storeCert',[])))
.subscribe(resp => this.getIdentities(resp));
}
private getIdentities(resp) {
this.statusMsg.next("Updating the list of available accounts")
this
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.http.get<any>(this.Base+'/sshagent',options)
.pipe(catchError(this.handleError('updateIdentities',[])))
.subscribe(resp => this.updateIdentities(resp));
}
private killAgent() {
this.statusMsg.next("Updating the list of available accounts")
this
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.http.delete<any>(this.Base+'/sshagent',options)
.pipe(catchError(this.handleError('updateIdentities',[])))
.subscribe(resp => this.updateIdentities(resp));
}
private updateIdentities(resp) {
let certcontents = resp;
console.log(certcontents);
this.identities = [];
console.log('identities updated');
let sites = this.computesite.getComputeSites();
for (let i in certcontents) {
let id = new Identity()
id.username = certcontents[i].Principals[0];
id.site = sites[0]
this.identities.push(id)
}
if (this.identities.length == 0) {
this.statusMsg.next(null);
return;
}
console.log(this.identities);
this.statusMsg.next('Updating Job list');
this.startPolling();
}
private startPolling() {
this.statusMsg.next(null);
if (!(this.timerSubscription === null)) {
console.log('unsubscribing timer');
this.timerSubscription.unsubscribe()
}
console.log('creating timer to poll for jobs');
// this.timerSubscription = timer(100,5000).subscribe(() => this.getJobs());
this.timerSubscription = timer(5000).pipe(repeat()).subscribe(() => this.getJobs());
this.getJobs();
}
public login() {
let redirect_uri = window.location.origin+"/sshauthz_callback";
let redirect_uri = window.location.origin+this.locationStrategy.getBaseHref()+"sshauthz_callback";
let state="asdfzxcv";
window.location.assign(this.sshauthz+"?response_type=token&redirect_uri="+redirect_uri+"&state="+state+"&client_id="+this.client_id);
}
// public testAuth() {
// if (this.sshcert === null) {
// this.authorised.next(false);
// } else {
// this.authorised.next(true);
// }
// this.testingAuth.next(false);
// // this.tesSelected.next(true);
// // this.Base = 'http://localhost:5000'
// // this.errMsg.next(null);
// //
// // let headers = new HttpHeaders();
// // this.testingAuth.next(true);
// // let options = { headers: headers, withCredentials: true};
// // this.http.get(this.Base+'/testauth',options).pipe(catchError(this.handleError('submit',[])))
// // .subscribe(resp => this.testAuthComplete(resp));
// }
public logout(): Boolean {
this.token = null;
this.sshcert = null;
this.errMsg.next(null);
this.statusMsg.next(null);
this.authorised.next(false);
this.killAgent();
if (!(this.timerSubscription === null)) {
this.timerSubscription.unsubscribe()
}
return true;
}
public connect(jobid: string,exechost: string,app: Strudelapp) {
this.errMsg.next(null);
let headers = new HttpHeaders();
let options = { headers: headers, withCredentials: true};
this.busy.next(true);
this.http.post<any>(this.Base+'/appsetup',app, options)
.pipe(catchError(this.handleError('submit',[])))
.subscribe(resp => { window.open(this.Base+"/connect/"+jobid+"/"+exechost); this.busy.next(false)});
}
public getErrmsgSubject(): BehaviorSubject<any> {
return this.errMsg;
public getstatusMsgSubject(): BehaviorSubject<any> {
return this.statusMsg;
}
......@@ -204,14 +297,12 @@ private handleError<T> (operation = 'operation', result?: T) {
this.sshcert = null;
if ( operation == 'getJobs') {
this.errMsg.next("Hmm, that didn't work. If you're using a local connection, please make sure Strudel-TES is running.");
this.statusMsg.next("Hmm, that didn't work. If you're using a local connection, please make sure Strudel-TES is running.");
}
if (operation == 'submit') {
this.errMsg.next("Hmm, I couldn't submit that job")
this.statusMsg.next("Hmm, I couldn't submit that job")
}
return of(result as T);
};
}
}
<p>
{{ errmsg }}
{{ statusMsg }}
</p>
......@@ -8,16 +8,16 @@ import { TesService } from '../tes.service';
})
export class TeserrorsComponent implements OnInit {
private errmsg: string;
public statusMsg: string;
constructor(private tesService: TesService,) { }
ngOnInit() {
this.tesService.getErrmsgSubject().subscribe( msg => this.getNewMessage(msg));
this.tesService.getstatusMsgSubject().subscribe( msg => this.getNewMessage(msg));
}
getNewMessage(msg) {
this.errmsg = msg;
this.statusMsg = msg;
}
}
......@@ -16,7 +16,7 @@ export class TokenextractorComponent implements OnInit {
}
ngOnInit() {
this.route.fragment.subscribe( frag => this.tesService.getCert(frag));
// this.route.fragment.subscribe( frag => this.tesService.setFragment(frag));
this.router.navigate(['/']);
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment