Skip to content

Commit

Permalink
NAS-133481 / 25.04 / Changes for VMs in new virtualization (#11323)
Browse files Browse the repository at this point in the history
  • Loading branch information
denysbutenko authored Jan 20, 2025
1 parent 3829331 commit 811d9a1
Show file tree
Hide file tree
Showing 11 changed files with 390 additions and 285 deletions.
6 changes: 3 additions & 3 deletions src/app/enums/virtualization.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,9 +55,9 @@ export const virtualizationDeviceTypeLabels = new Map<VirtualizationDeviceType,
[VirtualizationDeviceType.Usb, 'USB'],
[VirtualizationDeviceType.Tpm, 'TPM'],
[VirtualizationDeviceType.Disk, T('Disk')],
[VirtualizationDeviceType.Disk, T('GPU')],
[VirtualizationDeviceType.Disk, T('NIC')],
[VirtualizationDeviceType.Disk, T('Proxy')],
[VirtualizationDeviceType.Gpu, T('GPU')],
[VirtualizationDeviceType.Nic, T('NIC')],
[VirtualizationDeviceType.Proxy, T('Proxy')],
]);

export enum VirtualizationGpuType {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,20 @@
<ix-fieldset>
<ix-explorer
formControlName="source"
root="/"
[label]="'Source' | translate"
[required]="true"
[nodeProvider]="directoryNodeProvider"
[canCreateDataset]="isNew()"
></ix-explorer>

<ix-input
formControlName="destination"
[label]="'Destination' | translate"
[required]="true"
></ix-input>
@if (form.controls.destination.enabled) {
<ix-input
formControlName="destination"
[label]="'Destination' | translate"
[required]="true"
></ix-input>
}
</ix-fieldset>

<ix-form-actions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { mockApi, mockCall } from 'app/core/testing/utils/mock-api.utils';
import { mockAuth } from 'app/core/testing/utils/mock-auth.utils';
import { VirtualizationDeviceType } from 'app/enums/virtualization.enum';
import { VirtualizationDeviceType, VirtualizationType } from 'app/enums/virtualization.enum';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { SlideInRef } from 'app/modules/slide-ins/slide-in-ref';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
Expand Down Expand Up @@ -36,7 +36,7 @@ describe('InstanceDiskFormComponent', () => {
providers: [
mockProvider(SlideInRef, {
getData: () => ({
instanceId: 'my-instance',
instance: { id: 'my-instance', type: VirtualizationType.Container },
}),
close: jest.fn(),
requireConfirmationWhen: jest.fn(),
Expand Down Expand Up @@ -80,7 +80,7 @@ describe('InstanceDiskFormComponent', () => {
providers: [
mockProvider(SlideInRef, {
getData: () => ({
instanceId: 'my-instance',
instance: { id: 'my-instance', type: VirtualizationType.Container },
disk: {
name: 'existing-disk',
source: '/mnt/from',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import { MatCard, MatCardContent } from '@angular/material/card';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { Observable, of } from 'rxjs';
import { VirtualizationDeviceType } from 'app/enums/virtualization.enum';
import { VirtualizationDisk } from 'app/interfaces/virtualization.interface';
import { VirtualizationDeviceType, VirtualizationType } from 'app/enums/virtualization.enum';
import { VirtualizationDisk, VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxExplorerComponent } from 'app/modules/forms/ix-forms/components/ix-explorer/ix-explorer.component';
import { IxFieldsetComponent } from 'app/modules/forms/ix-forms/components/ix-fieldset/ix-fieldset.component';
Expand All @@ -22,7 +22,7 @@ import { ApiService } from 'app/modules/websocket/api.service';
import { FilesystemService } from 'app/services/filesystem.service';

interface InstanceDiskFormOptions {
instanceId: string;
instance: VirtualizationInstance;
disk: VirtualizationDisk | undefined;
}

Expand Down Expand Up @@ -50,7 +50,7 @@ export class InstanceDiskFormComponent implements OnInit {
private existingDisk = signal<VirtualizationDisk | null>(null);

protected readonly isLoading = signal(false);
protected readonly directoryNodeProvider = this.filesystem.getFilesystemNodeProvider({ directoriesOnly: false });
protected readonly directoryNodeProvider = this.filesystem.getFilesystemNodeProvider({ datasetsAndZvols: true });

protected form = this.formBuilder.nonNullable.group({
source: ['', Validators.required],
Expand All @@ -63,6 +63,10 @@ export class InstanceDiskFormComponent implements OnInit {
return !this.isNew() ? this.translate.instant('Edit Disk') : this.translate.instant('Add Disk');
});

protected get instance(): VirtualizationInstance {
return this.slideInRef.getData().instance;
}

constructor(
private formBuilder: FormBuilder,
private errorHandler: FormErrorHandlerService,
Expand All @@ -78,22 +82,22 @@ export class InstanceDiskFormComponent implements OnInit {
}

ngOnInit(): void {
const disk = this.slideInRef.getData().disk;
const disk = this.slideInRef.getData()?.disk;
if (disk) {
this.existingDisk.set(disk);
this.form.patchValue({
source: disk.source || '',
destination: disk.destination || '',
});
}
if (this.instance.type === VirtualizationType.Vm) {
this.form.controls.destination.disable();
}
}

onSubmit(): void {
this.isLoading.set(true);

const request$ = this.prepareRequest();

request$
this.prepareRequest()
.pipe(untilDestroyed(this))
.subscribe({
complete: () => {
Expand All @@ -112,19 +116,17 @@ export class InstanceDiskFormComponent implements OnInit {
}

private prepareRequest(): Observable<unknown> {
const instanceId = this.slideInRef.getData().instanceId;

const payload = {
...this.form.value,
dev_type: VirtualizationDeviceType.Disk,
} as VirtualizationDisk;

const existingDisk = this.existingDisk();
return existingDisk
? this.api.call('virt.instance.device_update', [instanceId, {
? this.api.call('virt.instance.device_update', [this.instance.id, {
...payload,
name: existingDisk.name,
}])
: this.api.call('virt.instance.device_add', [instanceId, payload]);
: this.api.call('virt.instance.device_add', [this.instance.id, payload]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ <h3 mat-card-title>
} @else {
@for (disk of visibleDisks(); track disk.name) {
<div class="disk">
<div>
{{ disk.source }}
{{ disk.destination }}
</div>
@if (disk.destination) {
<div>{{ disk.source }} → {{ disk.destination }}</div>
} @else {
<div>{{ disk.source }}</div>
}

<ix-device-actions-menu
[device]="disk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { MatButtonHarness } from '@angular/material/button/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { MockComponent } from 'ng-mocks';
import { of } from 'rxjs';
import { VirtualizationDeviceType } from 'app/enums/virtualization.enum';
import { VirtualizationDisk, VirtualizationProxy } from 'app/interfaces/virtualization.interface';
import { VirtualizationDeviceType, VirtualizationType } from 'app/enums/virtualization.enum';
import { VirtualizationDisk, VirtualizationInstance, VirtualizationProxy } from 'app/interfaces/virtualization.interface';
import { SlideIn } from 'app/modules/slide-ins/slide-in';
import {
InstanceDiskFormComponent,
Expand Down Expand Up @@ -44,7 +44,7 @@ describe('InstanceDisksComponent', () => {
providers: [
mockProvider(VirtualizationDevicesStore, {
isLoading: () => false,
selectedInstance: () => ({ id: 'my-instance' }),
selectedInstance: () => ({ id: 'my-instance', type: VirtualizationType.Container }),
devices: () => disks,
loadDevices: jest.fn(),
}),
Expand All @@ -65,7 +65,7 @@ describe('InstanceDisksComponent', () => {
const diskRows = spectator.queryAll('.disk');

expect(diskRows).toHaveLength(1);
expect(diskRows[0]).toHaveText('source-path → destination');
expect(diskRows[0]).toHaveText('/mnt/source-path → destination');
});

it('renders a menu to manage the disk', () => {
Expand All @@ -74,23 +74,51 @@ describe('InstanceDisksComponent', () => {
expect(actionsMenu[0].device).toBe(disks[0]);
});

it('opens disk form when Add is pressed', async () => {
const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' }));
await addButton.click();
describe('container', () => {
it('opens disk form when Add is pressed', async () => {
const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' }));
await addButton.click();

expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(
InstanceDiskFormComponent,
{ data: { disk: undefined, instanceId: 'my-instance' } },
);
expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(
InstanceDiskFormComponent,
{ data: { disk: undefined, instance: { id: 'my-instance', type: VirtualizationType.Container } as VirtualizationInstance } },
);
});

it('opens disk for for edit when actions menu emits (edit)', () => {
const actionsMenu = spectator.query(DeviceActionsMenuComponent)!;
actionsMenu.edit.emit();

expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(
InstanceDiskFormComponent,
{ data: { disk: disks[0], instance: { id: 'my-instance', type: VirtualizationType.Container } as VirtualizationInstance } },
);
});
});
describe('vm', () => {
it('opens disk form when Add is pressed', async () => {
jest.spyOn(spectator.inject(VirtualizationDevicesStore), 'selectedInstance')
.mockReturnValue({ id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance);

const addButton = await loader.getHarness(MatButtonHarness.with({ text: 'Add' }));
await addButton.click();

expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(
InstanceDiskFormComponent,
{ data: { disk: undefined, instance: { id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance } },
);
});

it('opens disk for for edit when actions menu emits (edit)', () => {
const actionsMenu = spectator.query(DeviceActionsMenuComponent)!;
actionsMenu.edit.emit();
it('opens disk for for edit when actions menu emits (edit)', () => {
jest.spyOn(spectator.inject(VirtualizationDevicesStore), 'selectedInstance')
.mockReturnValue({ id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance);
const actionsMenu = spectator.query(DeviceActionsMenuComponent)!;
actionsMenu.edit.emit();

expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(
InstanceDiskFormComponent,
{ data: { disk: disks[0], instanceId: 'my-instance' } },
);
expect(spectator.inject(SlideIn).open).toHaveBeenCalledWith(
InstanceDiskFormComponent,
{ data: { disk: disks[0], instance: { id: 'my-instance', type: VirtualizationType.Vm } as VirtualizationInstance } },
);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export class InstanceDisksComponent {
}

private openDiskForm(disk?: VirtualizationDisk): void {
this.slideIn.open(InstanceDiskFormComponent, { data: { disk, instanceId: this.deviceStore.selectedInstance().id } })
this.slideIn.open(InstanceDiskFormComponent, { data: { disk, instance: this.deviceStore.selectedInstance() } })
.pipe(filter((result) => !!result.response), untilDestroyed(this))
.subscribe(() => this.deviceStore.loadDevices());
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,21 @@
<ix-input
formControlName="cpu"
[label]="'CPU Configuration' | translate"
[hint]="containersHelptext.cpuHint | translate"
[required]="isVm()"
[hint]="isContainer() ? (containersHelptext.cpuHint | translate) : null"
></ix-input>

<ix-input
formControlName="memory"
[label]="'Memory Size' | translate"
[format]="formatter.memorySizeFormatting"
[parse]="formatter.memorySizeParsing"
[hint]="containersHelptext.memoryHint | translate"
[required]="isVm()"
[hint]="isContainer() ? (containersHelptext.memoryHint | translate) : null"
></ix-input>
</ix-form-section>

@if (isContainer()) {
<ix-form-section
[label]="'Environment' | translate"
[help]="'Environment' | translate"
Expand Down Expand Up @@ -95,6 +98,7 @@
}
</ix-list>
</ix-form-section>
}

<ix-form-section
[label]="'Disks' | translate"
Expand All @@ -115,17 +119,20 @@
>
<ix-explorer
formControlName="source"
root="/"
[label]="'Source' | translate"
[required]="true"
[nodeProvider]="directoryNodeProvider"
[nodeProvider]="directoryNodeProvider()"
[canCreateDataset]="true"
></ix-explorer>

<ix-input
formControlName="destination"
[label]="'Destination' | translate"
[required]="true"
></ix-input>
@if (isContainer()) {
<ix-input
formControlName="destination"
[label]="'Destination' | translate"
[required]="true"
></ix-input>
}
</ix-list-item>
}
</ix-list>
Expand Down
Loading

0 comments on commit 811d9a1

Please sign in to comment.