Commit 3e9650f0 authored by Marta Różańska's avatar Marta Różańska
Browse files

Merge pull request #49 in MEL/melodic-frontend from feature/MEL-985-ui-addbyon to rc3.0

* commit '30076f31':
  fix problem with UNKNOWN location for BYON
  info for strong password, fix problem with missing ip by SSH connection
  id for all ByonDefinition elements
  support for byon in ui
parents 376e8f5a 30076f31
......@@ -25,6 +25,11 @@ const routes: Routes = [
canActivate: [CommonUserAdminRoleGuard]
},
{
path: 'byon', loadChildren: './byon/byon.module#ByonModule',
canActivate: [CommonUserAdminRoleGuard]
},
{path: '**', loadChildren: './user/user.module#UserModule'},
];
......
......@@ -7,6 +7,7 @@ export enum IpVersion {
}
export class IpAddress {
id: number;
ipAddressType: IpAddressType;
ipVersion: string;
value: string;
......
export class LoginCredential {
id: number;
username: string;
password: string;
privateKey: string;
......
import {OperatingSystem} from '../../process/process-details/offer/model/operating-system';
import {GeoLocation} from '../../process/process-details/offer/model/geo-location';
export class NodeProperties {
id: number;
providerId: string;
numberOfCores: number;
memory: number;
disk: number;
operatingSystem: OperatingSystem;
geoLocation: GeoLocation;
}
import {TestBed} from '@angular/core/testing';
import {WebSshService} from './web-ssh.service';
describe('WebSshService', () => {
beforeEach(() => TestBed.configureTestingModule({}));
it('should be created', () => {
const service: WebSshService = TestBed.get(WebSshService);
expect(service).toBeTruthy();
});
});
import {Injectable} from '@angular/core';
import {AppConfigService} from '../../app-config/service/app-config.service';
import {LoginCredential} from '../model/login-credential';
@Injectable({
providedIn: 'root'
})
export class WebSshService {
constructor() {
}
createSshConnection(loginCredential: LoginCredential, publicIp: string, vmName: string) {
console.log(`Create SSH connection for vm: ${vmName}`);
if (loginCredential.privateKey) {
this.connectBySshWithKey(loginCredential, publicIp);
} else {
this.connectBySshWithPassword(loginCredential, publicIp);
}
}
private connectBySshWithKey(loginCredential: LoginCredential, publicIp: string) {
const formattedPrivateKey = this.formatKey(loginCredential.privateKey);
const webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${publicIp}&username=${loginCredential.username}` +
`&privatekey=${formattedPrivateKey}`;
console.log(`Key-based SSH connection under url: ${webSshUrl}`);
window.open(webSshUrl);
}
private connectBySshWithPassword(loginCredential: LoginCredential, publicIp: string) {
const webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${publicIp}&username=${loginCredential.username}` +
`&password=${btoa(loginCredential.password)}`;
console.log(`Password-based SSH connection under address: ${webSshUrl}`);
window.open(webSshUrl);
}
private formatKey(privateKey: string): string {
const lines = privateKey.split('\n');
let result = '';
for (const keyLine of lines) {
result += keyLine + '\\n';
}
return result.substring(0, result.length - 2); // remove last '\n' sign
}
}
......@@ -7,8 +7,8 @@ import {Cloud} from '../../process/process-details/offer/model/cloud';
import {ProcessOfferService} from '../../process/process-details/offer/service/process-offer.service';
import {UserService} from '../../user/service/user.service';
import {UserRole} from '../../user/model/user-role.enum';
import {AppConfigService} from '../../app-config/service/app-config.service';
import {OfferLocation} from '../../process/process-details/offer/model/offer-location';
import {WebSshService} from '../service/web-ssh.service';
@Component({
selector: 'app-vm-list',
......@@ -31,7 +31,8 @@ export class VmListComponent implements OnInit {
constructor(private applicationService: ApplicationService,
private processOfferService: ProcessOfferService,
private snackBar: MatSnackBar,
private userService: UserService) {
private userService: UserService,
private webSshService: WebSshService) {
}
ngOnInit() {
......@@ -107,10 +108,14 @@ export class VmListComponent implements OnInit {
}
getCity(vm: NodeCloudiator): string {
const locationId = vm.originId.split('/')[0];
const location = this.locationList
.find(value => value.id === locationId);
return (location && location.geoLocation) ? location.geoLocation.city : 'UNKNOWN';
if (vm.nodeProperties.geoLocation != null && vm.nodeProperties.geoLocation.city != null) {
return vm.nodeProperties.geoLocation.city;
} else { // find location by locationId
const locationId = vm.originId.split('/')[0];
const location = this.locationList
.find(value => value.id === locationId);
return (location && location.geoLocation) ? location.geoLocation.city : 'UNKNOWN';
}
}
userHasPermissionToSshConnection(vm: NodeCloudiator): boolean {
......@@ -118,28 +123,6 @@ export class VmListComponent implements OnInit {
}
onSshConnectionClick(vm: NodeCloudiator) {
console.log(`SSH connection click for vm: ${vm.name}`);
let webSshUrl;
if (vm.loginCredential.privateKey) {
console.log(`Key-based SSH connection`);
const formattedPrivateKey = this.formatKey(vm.loginCredential.privateKey);
webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${vm.publicIp}&username=${vm.loginCredential.username}` +
`&privatekey=${formattedPrivateKey}`;
window.open(webSshUrl);
} else {
webSshUrl = `${AppConfigService.settings.webSshUrl}/?hostname=${vm.publicIp}&username=${vm.loginCredential.username}` +
`&password=${btoa(vm.loginCredential.password)}`;
console.log(`Password-based SSH connection under address: ${webSshUrl}`);
window.open(webSshUrl);
}
}
private formatKey(privateKey: string): string {
const lines = privateKey.split('\n');
let result = '';
for (const keyLine of lines) {
result += keyLine + '\\n';
}
return result.substring(0, result.length - 2); // remove last '\n' sign
this.webSshService.createSshConnection(vm.loginCredential, vm.publicIp, vm.name);
}
}
mat-card {
padding: var(--margin-global);
margin: var(--double-margin);
}
mat-dialog-actions {
margin-bottom: var(--margin-global);
}
mat-form-field {
margin-left: var(--double-margin);
width: 20%;
}
#private-key-field {
width: 55%;
}
.mat-card-with-bigger-bottom {
padding-bottom: var(--quadruple-margin);
}
#provider-id-field {
width: 40%;
}
.node-properties-common-field {
width: 18%;
}
<mat-card>
<form #byonDefinitionFormId="ngForm" [formGroup]="byonDefinitionForm">
<mat-card-title>Byon definition</mat-card-title>
<mat-dialog-content>
<mat-card>
<mat-form-field>
<input matInput formControlName="name" placeholder="name" required>
<mat-error
*ngIf="form.name.hasError('required')">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
</mat-card>
<mat-card>
<mat-card-subtitle><h3>IP addresses</h3></mat-card-subtitle>
<mat-spinner *ngIf="byonEnumsLoadingInProgress"></mat-spinner>
<app-ip-address-list
[ipAddresses]="ipAddresses"
[byonEnums]="byonEnums"
[isReadMode]="isReadMode">
</app-ip-address-list>
</mat-card>
<mat-card class="mat-card-with-bigger-bottom">
<mat-card-subtitle><h3>Login credentials</h3></mat-card-subtitle>
<form #loginCredentialFormId="ngForm" [formGroup]="loginCredentialForm">
<mat-form-field>
<input matInput formControlName="username" placeholder="username" required>
<mat-error
*ngIf="loginCredentialFormControl.username.hasError('required') && (loginCredentialFormControl.username.dirty || loginCredentialFormControl.username.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="password" placeholder="password"
[type]="passwordHide ? 'password' : 'text'">
<mat-icon matSuffix (click)="passwordHide = !passwordHide">{{passwordHide ? 'visibility_off' :
'visibility'}}
</mat-icon>
<mat-error
*ngIf="loginCredentialFormControl.password.hasError('oneOfTwoFieldsRequired') && (loginCredentialFormControl.password.dirty || loginCredentialFormControl.password.touched)">
{{getPasswordOrKeyRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field id="private-key-field">
<mat-label>Private key</mat-label>
<textarea matInput
formControlName="privateKey"
cdkTextareaAutosize
#autosize="cdkTextareaAutosize"
cdkAutosizeMinRows="1"
cdkAutosizeMaxRows="10"></textarea>
<mat-error
*ngIf="loginCredentialFormControl.privateKey.hasError('oneOfTwoFieldsRequired') && (loginCredentialFormControl.privateKey.dirty || loginCredentialFormControl.privateKey.touched)">
{{getPasswordOrKeyRequiredMsg()}}
</mat-error>
</mat-form-field>
</form>
</mat-card>
<mat-card>
<mat-card-subtitle><h3>Node properties</h3></mat-card-subtitle>
<form #nodePropertiesFormId="ngForm" [formGroup]="nodePropertiesForm">
<mat-form-field id="provider-id-field">
<input matInput formControlName="providerId" placeholder="provider id" required>
<mat-error
*ngIf="nodePropertiesFormControl.providerId.hasError('required') && (nodePropertiesFormControl.providerId.dirty || nodePropertiesFormControl.providerId.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field class="node-properties-common-field">
<input matInput formControlName="numberOfCores" placeholder="number of cores" required>
<mat-error
*ngIf="nodePropertiesFormControl.numberOfCores.hasError('required') && (nodePropertiesFormControl.numberOfCores.dirty || nodePropertiesFormControl.numberOfCores.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="nodePropertiesFormControl.numberOfCores.hasError('pattern') && (nodePropertiesFormControl.numberOfCores.dirty || nodePropertiesFormControl.numberOfCores.touched)">
{{getNumberOfCoresMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field class="node-properties-common-field">
<input matInput formControlName="memory" placeholder="memory" required>
<mat-error
*ngIf="nodePropertiesFormControl.memory.hasError('required') && (nodePropertiesFormControl.memory.dirty || nodePropertiesFormControl.memory.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="nodePropertiesFormControl.memory.hasError('pattern') && (nodePropertiesFormControl.memory.dirty || nodePropertiesFormControl.memory.touched)">
{{getMemoryMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field class="node-properties-common-field">
<input matInput formControlName="disk" placeholder="disk" required>
<mat-error
*ngIf="nodePropertiesFormControl.disk.hasError('required') && (nodePropertiesFormControl.disk.dirty || nodePropertiesFormControl.disk.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="nodePropertiesFormControl.disk.hasError('pattern') && (nodePropertiesFormControl.disk.dirty || nodePropertiesFormControl.disk.touched)">
{{getDiskMsg()}}
</mat-error>
</mat-form-field>
<mat-card class="mat-card-with-bigger-bottom">
<mat-card-subtitle><h4>Operating system</h4></mat-card-subtitle>
<mat-spinner *ngIf="byonEnumsLoadingInProgress"></mat-spinner>
<form #operatingSystemFormId="ngForm" [formGroup]="operatingSystemForm">
<mat-form-field>
<mat-select formControlName="operatingSystemFamily" placeholder="operating system family"
name="operatingSystemFamily" required>
<mat-option *ngFor="let osFamily of byonEnums.osFamilies" [value]="osFamily">
{{osFamily}}
</mat-option>
</mat-select>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemFamily.hasError('required') && (operatingSystemFormControl.operatingSystemFamily.dirty || operatingSystemFormControl.operatingSystemFamily.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<mat-select formControlName="operatingSystemArchitecture" placeholder="operating system architecture"
name="operatingSystemArchitecture" required>
<mat-option *ngFor="let osArchitecture of byonEnums.osArchitectures" [value]="osArchitecture">
{{osArchitecture}}
</mat-option>
</mat-select>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemArchitecture.hasError('required') && (operatingSystemFormControl.operatingSystemArchitecture.dirty || operatingSystemFormControl.operatingSystemArchitecture.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="operatingSystemVersion" placeholder="operating system version"
required>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemVersion.hasError('required') && (operatingSystemFormControl.operatingSystemVersion.dirty || operatingSystemFormControl.operatingSystemVersion.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="operatingSystemFormControl.operatingSystemVersion.hasError('pattern') && (operatingSystemFormControl.operatingSystemVersion.dirty || operatingSystemFormControl.operatingSystemVersion.touched)">
{{getOsVersionMsg()}}
</mat-error>
</mat-form-field>
</form>
</mat-card>
<mat-card class="mat-card-with-bigger-bottom">
<mat-card-subtitle><h4>Geolocation</h4></mat-card-subtitle>
<form #geoLocationFormId="ngForm" [formGroup]="geoLocationForm">
<mat-form-field>
<input matInput formControlName="city" placeholder="city" required>
<mat-error
*ngIf="geoLocationFormControl.city.hasError('required') && (geoLocationFormControl.city.dirty || geoLocationFormControl.city.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="country" placeholder="country" required>
<mat-error
*ngIf="geoLocationFormControl.country.hasError('required') && (geoLocationFormControl.country.dirty || geoLocationFormControl.country.touched)">
{{getRequiredMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="latitude" placeholder="latitude" required>
<mat-error
*ngIf="geoLocationFormControl.latitude.hasError('required') && (geoLocationFormControl.latitude.dirty || geoLocationFormControl.latitude.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="(geoLocationFormControl.latitude.hasError('pattern') || geoLocationFormControl.latitude.hasError('min') || geoLocationFormControl.latitude.hasError('max')) && (geoLocationFormControl.latitude.dirty || geoLocationFormControl.latitude.touched)">
{{getLatitudeMsg()}}
</mat-error>
</mat-form-field>
<mat-form-field>
<input matInput formControlName="longitude" placeholder="longitude" required>
<mat-error
*ngIf="geoLocationFormControl.longitude.hasError('required') && (geoLocationFormControl.longitude.dirty || geoLocationFormControl.longitude.touched)">
{{getRequiredMsg()}}
</mat-error>
<mat-error
*ngIf="(geoLocationFormControl.longitude.hasError('pattern') || geoLocationFormControl.longitude.hasError('min') || geoLocationFormControl.longitude.hasError('max')) && (geoLocationFormControl.longitude.dirty || geoLocationFormControl.longitude.touched)">
{{getLongitudeMsg()}}
</mat-error>
</mat-form-field>
</form>
</mat-card>
</form>
</mat-card>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-raised-button type="button" color="warn" mat-dialog-close>
<mat-icon>undo</mat-icon>
Cancel
</button>
<button mat-raised-button [disabled]="byonDefinitionFormId.invalid || isReadMode"
(click)="saveByonDefinition(byonDefinitionFormId)"
color="primary">
<mat-icon>save</mat-icon>
Save
</button>
</mat-dialog-actions>
</form>
</mat-card>
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {ByonDefinitionFormComponent} from './byon-definition-form.component';
describe('ByonDefinitionFormComponent', () => {
let component: ByonDefinitionFormComponent;
let fixture: ComponentFixture<ByonDefinitionFormComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ByonDefinitionFormComponent]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ByonDefinitionFormComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});
import {Component, Inject, NgZone, OnInit, Optional, ViewChild} from '@angular/core';
import {FormBuilder, FormControl, FormGroup, NgForm, Validators} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef, MatSnackBar} from '@angular/material';
import {IpAddress} from '../../application/model/ip-address';
import {Byon} from '../model/byon';
import {CdkTextareaAutosize} from '@angular/cdk/text-field';
import {take} from 'rxjs/operators';
import {IpAddressListComponent} from '../ip-address-list/ip-address-list.component';
import {ByonEnums} from '../model/byon-enums';
import {ByonService} from '../service/byon.service';
import {oneOfTwoFieldsRequiredValidator} from '../validator/one-of-two-fields-required.validator';
@Component({
selector: 'app-byon-definition-form',
templateUrl: './byon-definition-form.component.html',
styleUrls: ['./byon-definition-form.component.css']
})
export class ByonDefinitionFormComponent implements OnInit {
@ViewChild(IpAddressListComponent) ipAddressesComponent: IpAddressListComponent;
@ViewChild('autosize', {read: false}) autosize: CdkTextareaAutosize;
byonData: Byon;
isReadMode = false;
passwordHide = true;
byonEnums = new ByonEnums();
byonEnumsLoadingInProgress = false;
ipAddresses = new Array<IpAddress>();
loginCredentialForm: FormGroup;
geoLocationForm: FormGroup;
operatingSystemForm: FormGroup;
byonDefinitionForm: FormGroup;
nodePropertiesForm: FormGroup;
constructor(private formBuilder: FormBuilder,
private dialogRef: MatDialogRef<ByonDefinitionFormComponent>,
private ngZone: NgZone,
private byonService: ByonService,
private snackBar: MatSnackBar,
@Optional() @Inject(MAT_DIALOG_DATA) public dialogData?: any) {
}
triggerResize() {
// Wait for changes to be applied, then trigger textarea resize.
this.ngZone.onStable.pipe(take(1))
.subscribe(() => this.autosize.resizeToFitContent(true));
}
ngOnInit() {
if (this.dialogData) {
this.byonData = this.dialogData ? this.dialogData.byonData : undefined;
this.isReadMode = this.dialogData ? this.dialogData.isReadMode : false;
this.ipAddresses = this.byonData ? this.byonData.ipAddresses : [];
}
this.byonEnumsLoadingInProgress = true;
this.byonService.getByonEnums().subscribe(byonEnumsResponse => {
this.byonEnums = byonEnumsResponse;
this.byonEnumsLoadingInProgress = false;
},
error1 => {
this.byonEnumsLoadingInProgress = false;
this.snackBar.open(`Error by getting available options for byons form: ${error1.error.message}`, 'Close');
}
);
this.loginCredentialForm = this.formBuilder.group({
id: this.byonData ? this.byonData.loginCredential.id : null,
username: this.byonData ? new FormControl({value: this.byonData.loginCredential.username, disabled: this.isReadMode})
: ['', Validators.required],
password: this.byonData ? new FormControl({value: this.byonData.loginCredential.password, disabled: this.isReadMode})
: [''],
privateKey: this.byonData ? new FormControl({value: this.byonData.loginCredential.privateKey, disabled: this.isReadMode})
: ['']
},
{
validators: oneOfTwoFieldsRequiredValidator('password', 'privateKey')
});
this.geoLocationForm = this.formBuilder.group({
id: this.byonData ? this.byonData.nodeProperties.geoLocation.id : null,
city: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.city, disabled: this.isReadMode})
: ['', Validators.required],
country: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.country, disabled: this.isReadMode})
: ['', Validators.required],
latitude: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.latitude, disabled: this.isReadMode})
: ['', [Validators.required, Validators.pattern(/^-*[0-9]*\.*[0-9]+$/),
Validators.min(-90), Validators.max(90)]], // double
longitude: this.byonData ? new FormControl({value: this.byonData.nodeProperties.geoLocation.longitude, disabled: this.isReadMode})
: ['', [Validators.required, Validators.pattern(/^-*[0-9]*\.*[0-9]+$/),
Validators.min(-180), Validators.max(180)]] // double
});
this.operatingSystemForm = this.formBuilder.group({
id: this.byonData ? this.byonData.nodeProperties.operatingSystem.id : null,
operatingSystemFamily: this.byonData ? new FormControl({
value: this.byonData.nodeProperties.operatingSystem.operatingSystemFamily,
disabled: this.isReadMode
}) : ['', Validators.required],
operatingSystemArchitecture: this.byonData ? new FormControl({
value:
this.byonData.nodeProperties.operatingSystem.operatingSystemArchitecture, disabled: this.isReadMode
}) : ['', Validators.required],
operatingSystemVersion: this.byonData ? new FormControl({
value: this.byonData.nodeProperties.operatingSystem.operatingSystemVersion,
disabled: this.isReadMode
}) : ['', [Validators.required, Validators.pattern(/^[0-9]*\.*[0-9]+$/)]] // bigDecimal
});
this.nodePropertiesForm = this.formBuilder.group({
id: this.byonData ? this.byonData.nodeProperties.id : null,
providerId: this.byonData ? new FormControl({value: this.byonData.nodeProperties.providerId, disabled: this.isReadMode})
: ['', Validators.required],
numberOfCores: this.byonData ? new FormControl({value: this.byonData.nodeProperties.numberOfCores, disabled: this.isReadMode})
: ['', [Validators.required, Validators.pattern(/^[0-9]+$/)]], // int
memory: this.byonData ? new FormControl({value: this.byonData.nodeProperties.memory, disabled: this.isReadMode})
: ['', [Validators.required, Validators.pattern(/^[0-9]+$/)]], // long
disk: this.byonData ? new FormControl({value: this.byonData.nodeProperties.disk, disabled: this.isReadMode})