Skip to content

Commit

Permalink
initial Proxmox console support (#526)
Browse files Browse the repository at this point in the history
* initial Proxmox console support

Added support for viewing Proxmox virtual machine NoVNC consoles
and sending a Ctrl+Alt+Del. Additional operations such as power on/off can be
added in the future.

- console route detects vm type and uses NoVNC for Proxmox or WMKS for vsphere
  • Loading branch information
sei-aschlackman authored Sep 20, 2022
1 parent eebda97 commit 52cbc37
Show file tree
Hide file tree
Showing 88 changed files with 3,526 additions and 1,192 deletions.
1,387 changes: 798 additions & 589 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"@datorama/akita": "^6.2.1",
"@datorama/akita-ng-router-store": "^6.0.2",
"@microsoft/signalr": "^5.0.8",
"@novnc/novnc": "^1.3.0",
"babel-polyfill": "^6.26.0",
"core-js": "^3.15.2",
"jquery": "^3.6.0",
Expand Down
10 changes: 8 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,15 @@ import { DialogService } from './services/dialog/dialog.service';
import { ErrorService } from './services/error/error.service';
import { NotificationService } from './services/notification/notification.service';
import { SystemMessageService } from './services/system-message/system-message.service';
import { VmService } from './state/vm/vm.service';
import { VsphereService } from './state/vsphere/vsphere.service';
import { ConsolePageComponent } from './components/console-page/console-page.component';
import { UserFollowPageComponent } from './components/user-follow-page/user-follow-page.component';
import { AkitaNgRouterStoreModule } from '@datorama/akita-ng-router-store';
import { environment } from '../environments/environment';
import { BASE_PATH } from './generated/vm-api';
import { NovncComponent } from './components/novnc/novnc.component';
import { ProxmoxConsoleComponent } from './components/proxmox/proxmox-console/proxmox-console.component';
import { OptionsBar2Component } from './components/options-bar2/options-bar2.component';

export const settings: ComnSettingsConfig = {
url: 'assets/config/settings.json',
Expand Down Expand Up @@ -83,6 +86,9 @@ const materialModules = [
SystemMessageComponent,
ConsolePageComponent,
UserFollowPageComponent,
NovncComponent,
ProxmoxConsoleComponent,
OptionsBar2Component,
],
imports: [
BrowserModule,
Expand All @@ -100,7 +106,7 @@ const materialModules = [
AppRoutingModule,
],
providers: [
VmService,
VsphereService,
SystemMessageService,
DialogService,
NotificationService,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/**
* Copyright 2021 Carnegie Mellon University. All Rights Reserved.
* Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
*/
*/
35 changes: 28 additions & 7 deletions src/app/components/console/console.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,32 @@
Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
-->

<div class="background">
<app-options-bar
[readOnly]="readOnly"
[vm]="vm$ | async"
[vmId]="vmId"
></app-options-bar>
<app-wmks [readOnly]="readOnly" [vmId]="vmId"></app-wmks>
<div class="container background">
<ng-container *ngIf="{ value: virtualMachine$ | async } as virtualMachine">
<ng-container
*ngIf="virtualMachine.value != null"
[ngSwitch]="virtualMachine.value.type"
>
<ng-container *ngSwitchCase="vmType.Proxmox">
<app-options-bar2
[vm]="virtualMachine.value"
[readOnly]="readOnly"
></app-options-bar2>
<app-proxmox-console
fxFill
[vm]="virtualMachine.value"
[readOnly]="readOnly"
></app-proxmox-console>
</ng-container>

<ng-container *ngSwitchDefault>
<app-options-bar
[readOnly]="readOnly"
[vm]="vsphereVm$ | async"
[vmId]="vmId"
></app-options-bar>
<app-wmks [readOnly]="readOnly" [vmId]="vmId"></app-wmks>
</ng-container>
</ng-container>
</ng-container>
</div>
5 changes: 5 additions & 0 deletions src/app/components/console/console.component.scss
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
left: 0px;
width: 100%;
}

.container {
height: 100%;
display: block
}
20 changes: 15 additions & 5 deletions src/app/components/console/console.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@

import { Component, Input } from '@angular/core';
import { Observable } from 'rxjs';
import { VsphereVirtualMachine } from '../../generated/vm-api';
import { VmQuery } from '../../state/vm/vm.query';
import { Vm, VmType, VsphereVirtualMachine } from '../../generated/vm-api';
import { VmService } from '../../state/vm/vm.service';
import { VsphereQuery } from '../../state/vsphere/vsphere.query';

@Component({
selector: 'app-console',
Expand All @@ -16,7 +17,12 @@ export class ConsoleComponent {

@Input() set vmId(value: string) {
this._vmId = value;
this.vm$ = this.vmQuery.selectEntity(value);
this.vsphereVm$ = this.vsphereQuery.selectEntity(value);
this.virtualMachine$ = this.vmService.get(value);
}

public get vmType(): typeof VmType {
return VmType;
}

get vmId(): string {
Expand All @@ -25,7 +31,11 @@ export class ConsoleComponent {

_vmId: string;

vm$: Observable<VsphereVirtualMachine>;
vsphereVm$: Observable<VsphereVirtualMachine>;
virtualMachine$: Observable<Vm>;

constructor(private vmQuery: VmQuery) {}
constructor(
private vsphereQuery: VsphereQuery,
private vmService: VmService
) {}
}
20 changes: 20 additions & 0 deletions src/app/components/novnc/novnc.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Copyright 2021 Carnegie Mellon University. All Rights Reserved.
Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
-->

<ng-container *ngIf="{ value: isConnected$ | async } as isConnected">
<div [hidden]="!isConnected.value === true">
<div #screen id="screen" class="background screen">
<!-- This is where the remote screen will appear -->
</div>
</div>

<div
*ngIf="isConnected.value === false"
class="align-items-center center align-content-center"
style="width: 70%"
>
<h1>Connecting...</h1>
</div>
</ng-container>
16 changes: 16 additions & 0 deletions src/app/components/novnc/novnc.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
Copyright 2021 Carnegie Mellon University. All Rights Reserved.
Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
*/

.center {
position: relative;
margin: auto;
top: 40%;
text-align: center;
}

// novnc element has to have a fixed height when scaleViewport is set
.screen {
height: 97vh;
}
30 changes: 30 additions & 0 deletions src/app/components/novnc/novnc.component.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
Copyright 2021 Carnegie Mellon University. All Rights Reserved.
Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
*/

import { ComponentFixture, TestBed } from '@angular/core/testing';

import { NovncComponent } from './novnc.component';

describe('NovncComponent', () => {
let component: NovncComponent;
let fixture: ComponentFixture<NovncComponent>;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ NovncComponent ]
})
.compileComponents();
});

beforeEach(() => {
fixture = TestBed.createComponent(NovncComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});
});
99 changes: 99 additions & 0 deletions src/app/components/novnc/novnc.component.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
Copyright 2021 Carnegie Mellon University. All Rights Reserved.
Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
*/

import {
AfterViewInit,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
ViewChild,
} from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { NoVNCService } from '../../services/novnc/novnc.service';

@Component({
selector: 'app-novnc',
templateUrl: './novnc.component.html',
styleUrls: ['./novnc.component.scss'],
})
export class NovncComponent implements OnChanges, AfterViewInit {
@Input() ticket: string;
@Input() url: string;
@Input() readOnly = false;

@Output() reconnect = new EventEmitter<number>();

private isConnectedSubject = new BehaviorSubject(false);
public isConnected$ = this.isConnectedSubject.asObservable();
private set isConnected(val: boolean) {
this.isConnectedSubject.next(val);
}

private failedConnectionAttempts = 0;
private backgroundColor: string;

@ViewChild('screen') screen: ElementRef;

constructor(private novncService: NoVNCService) {}

ngAfterViewInit() {
this.backgroundColor = getComputedStyle(
this.screen.nativeElement
).backgroundColor;
}

ngOnChanges(changes: SimpleChanges): void {
if (this.url && this.ticket && (changes['url'] || changes['ticket'])) {
this.startClient(this.url, this.ticket);
}

if (changes['readOnly']) {
this.novncService.setViewOnly(this.readOnly);
}
}

startClient(url: string, ticket: string) {
this.novncService.startClient(
url,
ticket,
'screen',
this.readOnly,
this.backgroundColor
);

this.novncService.setConnectListener(this.connected.bind(this));
this.novncService.setDisconnectListener(this.disconnected.bind(this));
this.novncService.setSecurityFailureListener(
this.securityFailure.bind(this)
);
}

connected(e) {
this.isConnected = true;
this.failedConnectionAttempts = 0;
}

// This function is called when we are disconnected
disconnected(e) {
this.isConnected = false;
this.failedConnectionAttempts++;
this.reconnect.emit(this.failedConnectionAttempts);

if (e.detail.clean) {
console.log('Disconnected');
} else {
console.log('Something went wrong, connection is closed');
console.log(e);
}
}

securityFailure(e) {
console.log(e);
}
}
4 changes: 2 additions & 2 deletions src/app/components/options-bar/options-bar.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
} from '../../models/vm/vm-model';
import { DialogService } from '../../services/dialog/dialog.service';
import { NotificationService } from '../../services/notification/notification.service';
import { VmService } from '../../state/vm/vm.service';
import { VsphereService } from '../../state/vsphere/vsphere.service';

declare var WMKS: any; // needed to check values
const MAX_COPY_RETRIES = 1;
Expand Down Expand Up @@ -71,7 +71,7 @@ export class OptionsBarComponent implements OnInit, OnDestroy {
private destroy$ = new Subject();

constructor(
public vmService: VmService,
public vmService: VsphereService,
public settingsService: ComnSettingsService,
private dialogService: DialogService,
private notificationService: NotificationService,
Expand Down
20 changes: 20 additions & 0 deletions src/app/components/options-bar2/options-bar2.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!--
Copyright 2022 Carnegie Mellon University. All Rights Reserved.
Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.
-->

<div class="options-container background">
<div class="options-content background">
<button mat-icon-button class="mat-small" [matMenuTriggerFor]="mainMenu">
<mat-icon class="text" svgIcon="gear" alt="Gear"></mat-icon>
</button>
<mat-menu #mainMenu="matMenu">
<button mat-menu-item [matMenuTriggerFor]="keyboardMenu">Keyboard</button>
<mat-menu #keyboardMenu="matMenu">
<button mat-menu-item (click)="ctrlAltDel()">Send Ctrl-Alt-Del</button>
</mat-menu>
</mat-menu>

<label class="vm-name text">{{ vm?.name }}</label>
</div>
</div>
43 changes: 43 additions & 0 deletions src/app/components/options-bar2/options-bar2.component.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright 2021 Carnegie Mellon University. All Rights Reserved.
// Released under a MIT (SEI)-style license. See LICENSE.md in the project root for license information.

a:visited {
color: black;
}

.mat-expansion-panel-header {
display: flex;
flex-direction: column;
}

.vm-name {
font-size: small;
margin-left: 10px;
}

.left {
float: left;
padding-bottom: 8px;
}

.mat-icon-button.mat-small {
line-height: 16px;
height: 16px;
width: 16px;
margin-top: 2px;
margin-left: 10px;
display: inline-block;
z-index: 1;
}

.mat-icon {
height: 18px;
width: 18px;
}

.mat-top-button {
font-size: x-small;
height: 21px;
line-height: 0px;
margin-left: 20px;
}
Loading

0 comments on commit 52cbc37

Please sign in to comment.