From d1d506ef8bb038bfe066bdc18bd29103332e0eee Mon Sep 17 00:00:00 2001 From: Chris Hines <chris.hines@monash.edu> Date: Mon, 23 Jul 2018 09:41:53 +1000 Subject: [PATCH] lots of development to sv2 --- src/app/app.component.html | 4 +- src/app/app.component.ts | 1 - src/app/app.module.ts | 11 +- src/app/computesite.ts | 14 + src/app/computesites.service.ts | 4 +- src/app/identity.ts | 35 ++- src/app/job/job.component.ts | 5 + src/app/joblist/joblist.component.html | 18 +- src/app/joblist/joblist.component.ts | 34 ++- src/app/launcher/launcher.component.html | 25 +- src/app/launcher/launcher.component.ts | 118 +++++--- src/app/logindialog/logindialog.component.css | 0 .../logindialog/logindialog.component.html | 3 + .../logindialog/logindialog.component.spec.ts | 25 ++ src/app/logindialog/logindialog.component.ts | 31 ++ .../logoutdialog/logoutdialog.component.css | 0 .../logoutdialog/logoutdialog.component.html | 3 + .../logoutdialog.component.spec.ts | 25 ++ .../logoutdialog/logoutdialog.component.ts | 30 ++ src/app/mock-compute-site.ts | 6 +- src/app/mock-strudel-app.ts | 10 +- src/app/strudelapps.service.ts | 13 +- src/app/tes.service.ts | 273 +++++++++++------- src/app/teserrors/teserrors.component.ts | 6 + 24 files changed, 496 insertions(+), 198 deletions(-) create mode 100644 src/app/logindialog/logindialog.component.css create mode 100644 src/app/logindialog/logindialog.component.html create mode 100644 src/app/logindialog/logindialog.component.spec.ts create mode 100644 src/app/logindialog/logindialog.component.ts create mode 100644 src/app/logoutdialog/logoutdialog.component.css create mode 100644 src/app/logoutdialog/logoutdialog.component.html create mode 100644 src/app/logoutdialog/logoutdialog.component.spec.ts create mode 100644 src/app/logoutdialog/logoutdialog.component.ts diff --git a/src/app/app.component.html b/src/app/app.component.html index 10e69ef..19425a3 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -4,8 +4,8 @@ <span class="fill-remaining-space"></span> <span class="fill-remaining-space"></span> <!-- <app-siteselection></app-siteselection> --> - <button mat-button *ngIf="authorised" (click)="logout()">Logout</button> - <button mat-button *ngIf="!authorised" (click)="login()">Login</button> + <!-- <button mat-button *ngIf="authorised" (click)="logout()">Logout</button> + <button mat-button *ngIf="!authorised" (click)="login()">Login</button> --> </mat-toolbar-row> </mat-toolbar> diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 963b7ef..8c6a42b 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -17,7 +17,6 @@ export class AppComponent { }; ngOnInit() { - setTimeout(() => { this.tesService.authorised.subscribe(authorised => { this.authorised = authorised }); }); // this.tesService.testingAuth.subscribe(testingAuth => { this.testingAuth = testingAuth; console.log('testingAuth updated'+this.testingAuth) }); // this.testingAuth = false; } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index e8ba57e..51a81f2 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -3,7 +3,7 @@ import { NgModule } from '@angular/core'; import { AppComponent } from './app.component'; -import { LauncherComponent, DialogPlaceholder } from './launcher/launcher.component'; +import { LauncherComponent } from './launcher/launcher.component'; import { JoblistComponent } from './joblist/joblist.component'; import { MatButtonModule } from '@angular/material'; import { MatFormFieldModule } from '@angular/material'; @@ -27,6 +27,8 @@ import { SiteselectionComponent } from './siteselection/siteselection.component' import { TeserrorsComponent } from './teserrors/teserrors.component'; import { AppRoutingModule } from './/app-routing.module'; import { TokenextractorComponent } from './tokenextractor/tokenextractor.component'; +import { LogindialogComponent } from './logindialog/logindialog.component'; +import { LogoutdialogComponent } from './logoutdialog/logoutdialog.component'; @@ -38,8 +40,9 @@ import { TokenextractorComponent } from './tokenextractor/tokenextractor.compone JobComponent, SiteselectionComponent, TeserrorsComponent, - DialogPlaceholder, - TokenextractorComponent + TokenextractorComponent, + LogindialogComponent, + LogoutdialogComponent ], imports: [ BrowserModule, @@ -60,7 +63,7 @@ import { TokenextractorComponent } from './tokenextractor/tokenextractor.compone AppRoutingModule, ], - entryComponents: [ DialogPlaceholder ], + entryComponents: [ LogindialogComponent, LogoutdialogComponent ], providers: [ StrudelappsService, ComputesitesService, TesService, MatDialog], bootstrap: [AppComponent] }) diff --git a/src/app/computesite.ts b/src/app/computesite.ts index 567f55a..a6de7f3 100644 --- a/src/app/computesite.ts +++ b/src/app/computesite.ts @@ -7,4 +7,18 @@ export class Computesite { cafingerprint: string; // Certificates contain a CA fingerprint. We use this // to figure out which compute site a certificate is valid // for + appCatalog: Strudelapp[]; +} + +export class Strudelapp { + 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; // does the application bind to a port on the localhost + // interface or on all interfaces. This behaviour determins + /// how we create tunnels } diff --git a/src/app/computesites.service.ts b/src/app/computesites.service.ts index 0a92e89..219535a 100644 --- a/src/app/computesites.service.ts +++ b/src/app/computesites.service.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { Computesite } from './computesite'; import { COMPUTESITES } from './mock-compute-site'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class ComputesitesService { constructor() { } diff --git a/src/app/identity.ts b/src/app/identity.ts index 94fd05a..952370b 100644 --- a/src/app/identity.ts +++ b/src/app/identity.ts @@ -1,7 +1,40 @@ import {Computesite} from './computesite'; -// Identities are defined by a username on a computer, but rather than just +// 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; + authservice: AuthService; + constructor( username: string, site: Computesite) { + this.username = username; + this.site = site; + } + displayName(): string { + return this.username+'@'+this.site.name; + } + repr(): string { + return JSON.stringify([this.username,this.site.cafingerprint,this.site.host]); + } +} + +export class AuthToken { + token: string; + authservice: AuthService; + constructor( token: string, authservice: AuthService ) { + this.token = token; + this.authservice = authservice; + } +} + +export class KeyCert { + key: string; + cert: string; + authservice: AuthService; +} + +export class AuthService { + base: string; + authorise: string; + sign: string; + client_id: string; } diff --git a/src/app/job/job.component.ts b/src/app/job/job.component.ts index cfcdbd3..c1518a7 100644 --- a/src/app/job/job.component.ts +++ b/src/app/job/job.component.ts @@ -19,15 +19,20 @@ export class JobComponent implements OnInit { } ngOnInit() { + console.log('creating job component'); if (this.jobdata.state == "RUNNING") { this.available = true; } else { this.available = false; } this.tesService.busy.subscribe(busy => this.busy = busy); + console.log('creating job component complete'); + } onCancel() { + this.jobdata.app = this.strudelAppsService.getApp(this.jobdata.name); + console.log(this.jobdata); this.tesService.cancel(this.jobdata); } diff --git a/src/app/joblist/joblist.component.html b/src/app/joblist/joblist.component.html index 7f6ea40..0ba48dc 100644 --- a/src/app/joblist/joblist.component.html +++ b/src/app/joblist/joblist.component.html @@ -1,8 +1,16 @@ -<div *ngIf="jobs.length == 0"> +<div *ngIf="identities.length == 0"> <mat-card> - You don't appear to have any jobs -</mat-card> + Click identity and login to a service to start an application. + </mat-card> </div> -<div *ngFor="let job of jobs"> - <app-job [jobdata]=job></app-job> +<div *ngFor="let id of identities"> + <mat-card> + <mat-card-title>{{ id.displayName() }}</mat-card-title> + <div *ngIf="jobs[id.repr()] == undefined || jobs[id.repr()].length == 0 "> + No jobs running yet + </div> + <div *ngFor="let job of jobs[id.repr()]"> + <app-job [jobdata]=job></app-job> + </div> +</mat-card> </div> diff --git a/src/app/joblist/joblist.component.ts b/src/app/joblist/joblist.component.ts index 21b0007..f27d908 100644 --- a/src/app/joblist/joblist.component.ts +++ b/src/app/joblist/joblist.component.ts @@ -2,6 +2,7 @@ import { Component, OnInit } from '@angular/core'; import {TesService} from '../tes.service'; import { Job } from '../job'; import { Observable } from 'rxjs/Observable'; +import { Identity } from '../identity'; @@ -11,16 +12,20 @@ import { Observable } from 'rxjs/Observable'; styleUrls: ['./joblist.component.css'] }) export class JoblistComponent implements OnInit { - public jobs: any[] = []; + public jobs: {[id: string]: Job[] } = {}; + public identities: Identity[]; private displayedColumns = ['id']; private jobsSubscription: any; + private idSubscription: any; - constructor(private tesService: TesService,) { } + constructor(private tesService: TesService,) { +} ngOnInit() { - - this.jobsSubscription = this.tesService.joblist.subscribe(jobs => this.jobs = jobs); - console.log("jobs subscribed by joblist component"); + console.log('creating joblist component'); + this.jobsSubscription = this.tesService.joblist.subscribe(jobs => this.updateJobs(jobs)); + this.idSubscription = this.tesService.identities.subscribe(ids => this.updateIds(ids)); + console.log("joblist component complete"); } public ngOnDestroy(): void { @@ -29,21 +34,18 @@ export class JoblistComponent implements OnInit { } } + updateIds(identities: Identity[]) { + console.log('update identities'); + this.identities = identities; + console.log('update identities complete'); - updateJobs(jobs) { - this.jobs = jobs; - console.log("job data received by the joblist component"); - console.log(this.jobs); } - login(){ - //Wire this up to the tes login component to get an ssh cert on the tes backend - //This will also tell the TES which site we are using - return; + updateJobs(jobs) { + console.log('updating jobs') + this.jobs = jobs; + console.log('update jobs complete') } - logout() { - return; - } } diff --git a/src/app/launcher/launcher.component.html b/src/app/launcher/launcher.component.html index 38c925b..5ad7440 100644 --- a/src/app/launcher/launcher.component.html +++ b/src/app/launcher/launcher.component.html @@ -1,5 +1,18 @@ -<div *ngIf="authorised"> +<mat-form-field> + <mat-select placeholder="Identity" (selectionChange)="selectId($event)"> + <mat-option *ngFor="let id of identities" [value]=id> + {{id.displayName()}} + </mat-option> + <mat-option> + <button mat-button (click)=login()>Login ... </button> + </mat-option> + <mat-option> + <button mat-button (click)=logout()> Logout </button> + </mat-option> + + </mat-select> +</mat-form-field> <mat-form-field> <mat-select placeholder="Application" [(value)]=app> <mat-option *ngFor="let a of strudelapps" [value]=a> @@ -7,12 +20,8 @@ </mat-option> </mat-select> </mat-form-field> -<button mat-button (click)=configureResources()>Configure Resources</button> -<button mat-button (click)=configureApp() [disabled]="!app">Configure App</button> -<button mat-button (click)=submitApp() [disabled]="!app">Start</button> +<button mat-button (click)=configureResources() [disabled]="!app || !identity">Configure Resources</button> +<!-- <button mat-button (click)=configureApp() [disabled]="!app || !identity">Configure App</button> --> +<button mat-button (click)=submitApp() [disabled]="!app || !identity">Start</button> <app-joblist></app-joblist> - - - -</div> diff --git a/src/app/launcher/launcher.component.ts b/src/app/launcher/launcher.component.ts index bbd4c4c..6c1db2c 100644 --- a/src/app/launcher/launcher.component.ts +++ b/src/app/launcher/launcher.component.ts @@ -3,7 +3,10 @@ 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'; +import { Identity } from '../identity'; +import { Computesite } from '../computesite'; +import { LogoutdialogComponent } from '../logoutdialog/logoutdialog.component'; +import { LogindialogComponent } from '../logindialog/logindialog.component' @@ -18,68 +21,93 @@ export class LauncherComponent implements OnInit { private app: Strudelapp; public authorised: boolean; private identity: Identity; + private identities: Identity[]; - constructor(private strudelappsService: StrudelappsService, public dialog: MatDialog, private tesService: TesService) { } - - ngOnInit() { - this.loadStrudelapps(); - setTimeout( () => { this.tesService.authorised.subscribe(auth => this.authorised = auth); }); + constructor( public dialog: MatDialog, private tesService: TesService) { } + ngOnInit() { + console.log('initialising launcher compoenent'); + this.strudelapps = []; + setTimeout( () => { this.tesService.identities.subscribe(identities => this.updateIdentities(identities)); }); + console.log('initialising launcher compoenent complete'); - configureApp() { - console.log('configuring app for',this.app.name); - this.openDialog('If the application takes any parameters, this will open a new window where you can configure them') } - configureResources() { - this.openDialog('This will open a new window to configure resource reqests') - } + updateIdentities(identities) { + console.log('updateIdentities in launcher'); + this.identities = identities; + console.log('updateIdentities in launcher complete'); - submitApp() { - console.log('submitting app for',this.app.name); - this.identity = this.tesService.identities[0]; - this.tesService.submit(this.app, this.identity); - // this.openDialog('This is where we should start the app'); } - appselect(a: any) { - console.log('appselect') - this.app = a; - } +login () { + let dialogRef = this.dialog.open(LogindialogComponent, { + width: '250px', + height: '400px', + }); + return; +} + +logout() { + let dialogRef = this.dialog.open(LogoutdialogComponent, { + width: '250px', + height: '400px', + }); + // this.tesService.logout(); + // return; +} - loadStrudelapps() { - this.strudelapps = this.strudelappsService.getStrudelapps() - } - openDialog(msg: string ): void { - let dialogRef = this.dialog.open(DialogPlaceholder, { - width: '250px', - height: '400px', - data: { 'msg': msg } - }); - dialogRef.afterClosed().subscribe(result => { - console.log('The dialog was closed'); - }); + selectId(event: any) { + console.log('in selectID'); + if (!(event.value === undefined) && event.value instanceof(Identity)) { + this.identity=event.value; + this.strudelapps = this.identity.site.appCatalog; } + console.log('selectID complete'); + } + configureResources() { + let configwindow = window.open(this.identity.site.url+'configure/'+this.app.name); + } -} + submitApp() { + this.tesService.getconfig(this.app, this.identity) + .subscribe(resp => {console.log(resp); this.tesService.submit(this.app,this.identity,resp)}); + } -@Component({ - selector: 'dialog-placeholder', - templateUrl: 'dialog-placeholder.html', -}) -export class DialogPlaceholder { - constructor( - public dialogRef: MatDialogRef<DialogPlaceholder>, - @Inject(MAT_DIALOG_DATA) public data: any) { } - onNoClick(): void { - this.dialogRef.close(); - } + // openDialog(msg: string ): void { + // let dialogRef = this.dialog.open(LoginDialog, { + // width: '250px', + // height: '400px', + // data: { 'msg': msg } + // }); + // + // dialogRef.afterClosed().subscribe(result => { + // console.log('The dialog was closed'); + // }); + // } + } + +// @Component({ +// selector: 'dialog-placeholder', +// templateUrl: 'dialog-placeholder.html', +// }) +// export class LoginDialog { +// +// constructor( +// public dialogRef: MatDialogRef<LoginDialog>, +// @Inject(MAT_DIALOG_DATA) public data: any) { } +// +// onNoClick(): void { +// this.dialogRef.close(); +// } +// +// } diff --git a/src/app/logindialog/logindialog.component.css b/src/app/logindialog/logindialog.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/logindialog/logindialog.component.html b/src/app/logindialog/logindialog.component.html new file mode 100644 index 0000000..e83844b --- /dev/null +++ b/src/app/logindialog/logindialog.component.html @@ -0,0 +1,3 @@ + + <button mat-button (click)="onLogin()">Login to M3</button> + <button mat-button (click)="onCancel()">Cancel</button> diff --git a/src/app/logindialog/logindialog.component.spec.ts b/src/app/logindialog/logindialog.component.spec.ts new file mode 100644 index 0000000..39047f4 --- /dev/null +++ b/src/app/logindialog/logindialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogindialogComponent } from './logindialog.component'; + +describe('LogindialogComponent', () => { + let component: LogindialogComponent; + let fixture: ComponentFixture<LogindialogComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LogindialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LogindialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/logindialog/logindialog.component.ts b/src/app/logindialog/logindialog.component.ts new file mode 100644 index 0000000..b961709 --- /dev/null +++ b/src/app/logindialog/logindialog.component.ts @@ -0,0 +1,31 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material'; +import { TesService } from '../tes.service'; + + + +@Component({ + selector: 'app-logindialog', + templateUrl: './logindialog.component.html', + styleUrls: ['./logindialog.component.css'] +}) +export class LogindialogComponent implements OnInit { + + constructor( + public dialogRef: MatDialogRef<LogindialogComponent>, + @Inject(MAT_DIALOG_DATA) public data: any, + private tesService: TesService) { + } + + ngOnInit() { + } + + onLogin() { + this.tesService.login(); + } + onCancel() { + this.dialogRef.close(); + } + + +} diff --git a/src/app/logoutdialog/logoutdialog.component.css b/src/app/logoutdialog/logoutdialog.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/logoutdialog/logoutdialog.component.html b/src/app/logoutdialog/logoutdialog.component.html new file mode 100644 index 0000000..b1d2c8a --- /dev/null +++ b/src/app/logoutdialog/logoutdialog.component.html @@ -0,0 +1,3 @@ + +<button mat-button (click)="onLogout()">Logout of everything</button> +<button mat-button (click)="onCancel()">Cancel</button> diff --git a/src/app/logoutdialog/logoutdialog.component.spec.ts b/src/app/logoutdialog/logoutdialog.component.spec.ts new file mode 100644 index 0000000..15d64c3 --- /dev/null +++ b/src/app/logoutdialog/logoutdialog.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { LogoutdialogComponent } from './logoutdialog.component'; + +describe('LogoutdialogComponent', () => { + let component: LogoutdialogComponent; + let fixture: ComponentFixture<LogoutdialogComponent>; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ LogoutdialogComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(LogoutdialogComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/logoutdialog/logoutdialog.component.ts b/src/app/logoutdialog/logoutdialog.component.ts new file mode 100644 index 0000000..53a5c8a --- /dev/null +++ b/src/app/logoutdialog/logoutdialog.component.ts @@ -0,0 +1,30 @@ +import { Component, OnInit, Inject } from '@angular/core'; +import { TesService } from '../tes.service'; +import { MatDialog, MatDialogRef, MAT_DIALOG_DATA, MatDialogModule } from '@angular/material'; + + +@Component({ + selector: 'app-logoutdialog', + templateUrl: './logoutdialog.component.html', + styleUrls: ['./logoutdialog.component.css'] +}) +export class LogoutdialogComponent implements OnInit { + + constructor( + public dialogRef: MatDialogRef<LogoutdialogComponent>, + @Inject(MAT_DIALOG_DATA) public data: any, + private tesService: TesService) { + } + + ngOnInit() { + } + + onLogout() { + this.tesService.logout(); + this.dialogRef.close(); + } + onCancel() { + this.dialogRef.close(); + } + +} diff --git a/src/app/mock-compute-site.ts b/src/app/mock-compute-site.ts index c2a9b36..26e6050 100644 --- a/src/app/mock-compute-site.ts +++ b/src/app/mock-compute-site.ts @@ -1,7 +1,9 @@ import { Computesite } from './computesite'; +import { STRUDELAPPS } from './mock-strudel-app'; export const COMPUTESITES: Computesite[] = [ - { url: 'http://localhost:8080/config', name: 'M3', + { url: 'https://vm-118-138-240-255.erc.monash.edu.au/m3siteconfig/', name: 'M3', host: 'm3.massive.org.au', - cafingerprint: 'RSA SHA256:cmDxHrZQSPlBMUUcI/BWmruXho1XOzfXPDHSqVTwV2I' }, + cafingerprint: 'RSA SHA256:cmDxHrZQSPlBMUUcI/BWmruXho1XOzfXPDHSqVTwV2I', + appCatalog: STRUDELAPPS } ]; diff --git a/src/app/mock-strudel-app.ts b/src/app/mock-strudel-app.ts index 5045b19..3feadbf 100644 --- a/src/app/mock-strudel-app.ts +++ b/src/app/mock-strudel-app.ts @@ -1,4 +1,4 @@ -import { Strudelapp } from './strudelapp'; +import { Strudelapp } from './computesite'; const tmuxscript = `#!/bin/bash #SBATCH -J tmux @@ -55,10 +55,10 @@ const tmuxclient = {'cmd': ['/usr/bin/gnome-terminal', '--','ssh','-t', const jupyterclient = {'cmd': null, 'redir': '?token={token}'} export const STRUDELAPPS: Strudelapp[] = [ - { url: null, name: 'tmux', startscript: tmuxscript, - paramscmd: '/home/chines/smuxparams.py', client: tmuxclient, localbind: false }, + // { url: null, name: 'tmux', startscript: tmuxscript, + // paramscmd: '/home/chines/smuxparams.py', client: tmuxclient, localbind: false }, { url: null, name: 'Jupyter Notebook', startscript: jupyterscript, paramscmd: '/home/chines/jupyter_params.py', client: jupyterclient, localbind: true }, - { url: null, name: 'Standard Desktop', startscript: stddesktop, - paramscmd: '/home/chines/desktop_params.py', client: vncviewer, localbind: false }, + // { url: null, name: 'Standard Desktop', startscript: stddesktop, + // paramscmd: '/home/chines/desktop_params.py', client: vncviewer, localbind: false }, ]; diff --git a/src/app/strudelapps.service.ts b/src/app/strudelapps.service.ts index 8b767a3..30febe9 100644 --- a/src/app/strudelapps.service.ts +++ b/src/app/strudelapps.service.ts @@ -2,7 +2,9 @@ import { Injectable } from '@angular/core'; import { Strudelapp } from './strudelapp'; import { STRUDELAPPS } from './mock-strudel-app'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class StrudelappsService { constructor() { } @@ -11,15 +13,6 @@ export class StrudelappsService { } getApp(name: string) { - if (name.match(/mux/)) { - return STRUDELAPPS[0]; - } - if (name.match(/upyter/)) { - return STRUDELAPPS[1]; - } - if (name.match(/desktop/)) { - return STRUDELAPPS[2]; - } return STRUDELAPPS[0]; } diff --git a/src/app/tes.service.ts b/src/app/tes.service.ts index 66f8ca3..0362dea 100644 --- a/src/app/tes.service.ts +++ b/src/app/tes.service.ts @@ -5,9 +5,11 @@ import { of } from 'rxjs/observable/of'; import { catchError, map, tap } from 'rxjs/operators'; import { Job } from './job'; import {BehaviorSubject} from 'rxjs/BehaviorSubject'; +import {Subject} from 'rxjs/Subject'; + import { Strudelapp } from './strudelapp'; import { Computesite } from './computesite'; -import { Identity } from './identity'; +import { Identity, AuthToken, KeyCert, AuthService } from './identity'; import { BatchInterface} from './batchinterface'; import {ComputesitesService} from './computesites.service'; import { StrudelappsService } from './strudelapps.service'; @@ -17,25 +19,28 @@ 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 { ActivatedRoute, Router } from '@angular/router'; // import keypair = require('keypair'); // import forge = require('node-forge'); -@Injectable() +@Injectable({ + providedIn: 'root', +}) 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>; +// 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 statusMsg: BehaviorSubject<any>; public jobs: any[]; public busy: BehaviorSubject<boolean> ; -public testingAuth: BehaviorSubject<boolean>; -public joblist: BehaviorSubject<Job[]>; +// public testingAuth: BehaviorSubject<boolean>; +public joblist: BehaviorSubject<{ [id: string ]: Job[]}>; private timerSubscription: any; -private token: string; +private token: Subject<AuthToken>; +private keyCert: Subject<KeyCert>; private signing_endpoint: string; private sshauthzbase: string; private sshauthz: string; @@ -43,42 +48,42 @@ private client_id: string; private keypair: any; private ssh: any; private sshcert: any; -private identities: Identity[]; -private batchinterface: BatchInterface; +public identities: BehaviorSubject<Identity[]>; +private batchinterface: {[id: string] : BatchInterface}; constructor(private http: HttpClient, private locationStrategy: LocationStrategy, private route: ActivatedRoute, + private router: Router, 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); this.busy = new BehaviorSubject<boolean>(false); - this.joblist = new BehaviorSubject<Job[]>([]); + this.joblist = new BehaviorSubject<{[id: string]: Job[]}>({}); this.timerSubscription = null; - this.token = null; - this.sshauthzbase = "https://autht.massive.org.au/hpcid/"; - 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 } + this.token = new Subject<AuthToken>(); + this.keyCert = new Subject<KeyCert>(); + this.identities= new BehaviorSubject<Identity[]>([]); + this.route.fragment.subscribe(frag => this.storeToken(frag)); + this.token.subscribe(token => this.getCert(token)); + this.keyCert.subscribe(keyCert => this.sshAdd(keyCert)); + this.batchinterface = {}; + this.getIdentities(); } - updateJoblist(resp) { + updateJoblist(resp, identity: Identity) { let joblist = <Job[]>resp; + let alljobs = this.joblist.value; + let i = 0; for (let j of joblist) { - j.app = this.strudelappsService.getApp(j.name) + j.app = this.strudelappsService.getApp(j.name); + j.identity = identity; } - this.joblist.next(joblist); + alljobs[identity.repr()] = joblist; + this.joblist.next(alljobs); this.statusMsg.next(null); } @@ -96,6 +101,7 @@ private batchinterface: BatchInterface; params.set('interface',interfacestr); let identitystr = JSON.stringify(identity); params.set('identity',identitystr); + return params.toString(); } @@ -103,27 +109,52 @@ private batchinterface: BatchInterface; let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; 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) + // remove from the job list any jobs for identities that we don't know about + let oldjobs = this.joblist.value; + let oldjobkeys = Object.keys(oldjobs); + let ids = [] + for (let identity of this.identities.value) { + ids.push(identity.repr()); + } + for (let id of oldjobkeys) { + if (!ids.includes(id)) { + delete oldjobs[id]; + } + } + this.joblist.next(oldjobs); + // for identities that we do know about, query the job list + for (let identity of this.identities.value) { + if (this.batchinterface[identity.repr()] === undefined) { + this.getconfig(new Strudelapp(),identity) + .subscribe(resp => this.batchinterface[identity.repr()] = resp); + continue; + } + let paramstr = this.buildParams(null,identity,this.batchinterface[identity.repr()]); + this.http.get<Job[]>(this.Base+'/stat'+'?'+paramstr,options) .pipe(catchError(this.handleError('getJobs',[]))) - .subscribe(resp => this.updateJoblist(resp)); + .subscribe(resp => this.updateJoblist(resp,identity)); } + } + getconfig(app: Strudelapp, identity: Identity): Observable<any> { + let headers = new HttpHeaders(); + let options = { headers: headers, withCredentials: true}; + return this.http.get<any>(identity.site.url+'getconfig/'+app.name,options) + .pipe(catchError(this.handleError('getconfig',[]))) - submit(app: Strudelapp, identity: Identity) { - console.log("In tes submit url "+this.Base+'/submit'); - console.log("starting app"+app.name); + +} + + submit(app: Strudelapp, identity: Identity, batchinterface: BatchInterface) { let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; this.statusMsg.next('Submitting job'); this.busy.next(true); - let paramstr = this.buildParams(app,identity,this.batchinterface); + let paramstr = this.buildParams(app,identity,batchinterface); this.http.post<any>(this.Base+'/submit'+'?'+paramstr,{}, options) .pipe(catchError(this.handleError('submit',[]))) .subscribe(resp => this.busy.next(false)); - } cancel(job: Job) { @@ -132,10 +163,13 @@ private batchinterface: BatchInterface; let options = { headers: headers, withCredentials: true}; this.statusMsg.next(null); let data = {}; - let paramstr = this.buildParams(job.app,job.identity,this.batchinterface); + console.log(job.identity); + console.log(job.identity.repr); + let paramstr = this.buildParams(job.app,job.identity,this.batchinterface[job.identity.repr()]); this.http.delete<any>(this.Base+'/cancel/'+job.jobid+'?'+paramstr, options) .pipe(catchError(this.handleError('cancel',[]))) .subscribe(resp => this.submitted(resp)); + console.log('tes.cancel complete'); } public connect(job: Job) { @@ -143,81 +177,112 @@ private batchinterface: BatchInterface; 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 paramstr = this.buildParams(job.app,job.identity,this.batchinterface[job.identity.repr()]); let appwindow = window.open(this.Base+"/connect/"+job.jobid+"/"+job.batch_host+"?"+paramstr); appwindow.focus(); this.busy.next(false); } +storeToken(frag: string) { + if (frag === undefined || frag == null) { + return; + } + let tokenmatch = null; + let statematch = null; + if (!(frag === undefined) && !(frag == null)) { + tokenmatch = frag.match(/access_token\=([\S\s]*?)[&|$]/); + statematch = frag.match(/state\=([\S\s]*?)[&|$]/); + } + if (tokenmatch == null || statematch == null) { + return; + } + + let accesstoken = tokenmatch[1]; + let state = statematch[1]; + this.router.navigate(['/']); + + //Verify that the state matched the nonce we used when initiating login + let tuple = JSON.parse(localStorage.getItem('authservice')); + if (tuple[1] != state) { + return + } + + this.token.next(new AuthToken(tokenmatch[1],tuple[0])); + + // TODO fire off a query to the auth service to get the associated sites -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 argument is',frag); - console.log('fragment.getvalue is') - console.log(frag) - let match = frag.match(/access_token\=([\S\s]*?)[&|$]/); - if (match == null) { - return; + +} + +getCert(token: AuthToken) { + console.log('in getCert'); + if (token.token === undefined || token.token === '' || token.token == null) { + console.log('no authtoken available, we wont be able to generate a cert'); + console.log(token); + return } - this.authorised.next(true); - console.log(match); - this.token = match[1]; - console.log(this.token); - // this.token = frag; - console.log('tes stored the token'+this.token); - - // let data = {'token': this.token, 'sshauthz_endpoint': this.signing_endpoint} - // console.log(data); - console.log("Generating key matter") + 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 newkeypair = keypair(); + let publicKey = forge.pki.publicKeyFromPem(newkeypair.public); + let sshpub = 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}; - let data = {'token': this.token, 'pubkey': this.ssh, 'signing_url': this.signing_endpoint}; + let data = {'token': token.token, 'pubkey': sshpub, 'signing_url': token.authservice.sign}; 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',[]))) - .subscribe(resp => this.storeCert(resp)); + .subscribe(resp => this.makeKeyCert(newkeypair.private, resp, token.authservice)) + console.log('getcert complete'); +} + +makeKeyCert(key: string, resp, authservice: AuthService) { + let keyCert = new KeyCert() + keyCert.key = key; + keyCert.cert = resp['cert']; + keyCert.authservice = authservice; + console.log('updating keycert',keyCert); + this.keyCert.next(keyCert); } -private storeCert(resp) { - console.log(resp); - this.busy.next(false); - this.authorised.next(true); - this.testingAuth.next(false); +private sshAdd(keyCert: KeyCert) { - console.log(resp['cert']); - this.sshcert = resp['cert']; + console.log('in sshAdd'); + if (keyCert.key == undefined) { + return; + } let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; this.statusMsg.next("Authorising ...") - let data = {'key': this.keypair.private, 'cert': this.sshcert}; + let data = {'key': keyCert.key, 'cert': keyCert.cert}; + console.log('adding key',data); this.http.post<any>(this.Base+'/sshagent',data,options) .pipe(catchError(this.handleError('storeCert',[]))) - .subscribe(resp => this.getIdentities(resp)); + .subscribe(resp => this.getIdentities(), + error => this.httperror(error)); + console.log('sshAdd complete'); } -private getIdentities(resp) { +private getIdentities() { + console.log('retrieving current identities'); 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',[]))) + .pipe(catchError(this.handleError('getIdentities',[]))) .subscribe(resp => this.updateIdentities(resp)); + console.log('getIdentities complete'); } private killAgent() { @@ -226,32 +291,35 @@ private killAgent() { let headers = new HttpHeaders(); let options = { headers: headers, withCredentials: true}; this.http.delete<any>(this.Base+'/sshagent',options) - .pipe(catchError(this.handleError('updateIdentities',[]))) + .pipe(catchError(this.handleError('killAgent',[]))) .subscribe(resp => this.updateIdentities(resp)); } private updateIdentities(resp) { + //TODO Each cert as the signing CA parameter. Use this to find the compute sites + // rather than just assuming sites[0] + console.log('attempting to update our local list of identities'); let certcontents = resp; console.log(certcontents); - this.identities = []; + let identities = []; console.log('identities updated'); let sites = this.computesite.getComputeSites(); + let idsShort = [] for (let i in certcontents) { - let id = new Identity() - id.username = certcontents[i].Principals[0]; - id.site = sites[0] - this.identities.push(id) + identities.push(new Identity(certcontents[i].Principals[0],sites[0])) } - if (this.identities.length == 0) { + this.identities.next(identities); + if (identities.length == 0) { this.statusMsg.next(null); + console.log('local identities, none available'); return; } - console.log(this.identities); - this.statusMsg.next('Updating Job list'); + console.log('local identities updated'); this.startPolling(); } private startPolling() { + console.log('in start polling'); this.statusMsg.next(null); if (!(this.timerSubscription === null)) { console.log('unsubscribing timer'); @@ -262,23 +330,29 @@ private startPolling() { this.timerSubscription = timer(5000).pipe(repeat()).subscribe(() => this.getJobs()); this.getJobs(); + console.log('start polling complete'); } public login() { 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); + let nonce="asdfzxcv"; + let authservice = new AuthService(); + authservice.base = "https://autht.massive.org.au/hpcid/"; + authservice.authorise = authservice.base + 'oauth/authorize'; + authservice.sign = authservice.base + 'api/v1/sign_key'; + authservice.client_id = "86c06039-e589-4b39-9d1f-9eca431be18f"; + localStorage.setItem('authservice', JSON.stringify([authservice,nonce])); + window.location.assign(authservice.authorise+"?response_type=token&redirect_uri="+redirect_uri+"&state="+nonce+"&client_id="+authservice.client_id); } public logout(): Boolean { this.token = null; this.sshcert = null; this.statusMsg.next(null); - this.authorised.next(false); this.killAgent(); - if (!(this.timerSubscription === null)) { - this.timerSubscription.unsubscribe() - } + // if (!(this.timerSubscription === null)) { + // this.timerSubscription.unsubscribe() + // } return true; } @@ -287,20 +361,23 @@ public getstatusMsgSubject(): BehaviorSubject<any> { return this.statusMsg; } +private httperror(error: any) { + console.log(error); +} private handleError<T> (operation = 'operation', result?: T) { return (error: any): Observable<T> => { - // TODO: send the error to remote logging infrastructure - console.error(error); // log to console instead - this.authorised.next(false); - this.token = null; - this.sshcert = null; + console.log('in handle error',operation); + console.log(error.status); if ( operation == 'getJobs') { 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') { + } else if (operation == 'submit') { this.statusMsg.next("Hmm, I couldn't submit that job") + } else if (operation == 'getIdentities') { + this.statusMsg.next("Hmm, I coudln't get any data from the backend. Try refreshing this page") + } else { + console.error(error); } return of(result as T); }; diff --git a/src/app/teserrors/teserrors.component.ts b/src/app/teserrors/teserrors.component.ts index 068b8cd..607b7d1 100644 --- a/src/app/teserrors/teserrors.component.ts +++ b/src/app/teserrors/teserrors.component.ts @@ -13,11 +13,17 @@ export class TeserrorsComponent implements OnInit { constructor(private tesService: TesService,) { } ngOnInit() { + console.log('constructing message component'); this.tesService.getstatusMsgSubject().subscribe( msg => this.getNewMessage(msg)); + console.log('constructing message component complete'); + } getNewMessage(msg) { + console.log('teserrors get message'); this.statusMsg = msg; + console.log('teserrors get message complete'); + } } -- GitLab