Skip to content

Commit

Permalink
NAS-134393 / 25.04.0 / Expose IO bus configuration in the UI (by Alex…
Browse files Browse the repository at this point in the history
…Karpov98) (#11671)

* Empty commit to create PR on github.

You should reset it

* NAS-134393: PR update

---------

Co-authored-by: Alex Karpov <akarpov@ixsystems.com>
  • Loading branch information
bugclerk and AlexKarpov98 authored Mar 6, 2025
1 parent eebaccd commit 3885332
Show file tree
Hide file tree
Showing 105 changed files with 571 additions and 218 deletions.
12 changes: 12 additions & 0 deletions src/app/enums/virtualization.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@ export const virtualizationTypeLabels = new Map<VirtualizationType, string>([
[VirtualizationType.Vm, T('VM')],
]);

export enum DiskIoBus {
Nvme = 'NVME',
VirtioBlk = 'VIRTIO-BLK',
VirtioScsi = 'VIRTIO-SCSI',
}

export const diskIoBusLabels = new Map<DiskIoBus, string>([
[DiskIoBus.Nvme, 'NVMe'],
[DiskIoBus.VirtioBlk, 'Virtio-BLK'],
[DiskIoBus.VirtioScsi, 'Virtio-SCSI'],
]);

export const virtualizationTypeIcons = [
{
value: VirtualizationType.Container,
Expand Down
5 changes: 5 additions & 0 deletions src/app/helptext/virtualization/containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ Choose a VM for full OS isolation, kernel independence, and running diverse OS t
host_port_placeholder: T('Host Port'),
host_port_tooltip: T('Specify the host port to be mapped to the container\'s port.'),

io_bus_tooltip: T('Choose the disk I/O bus type that best suits your system’s needs:\
<br /><br /> • NVMe – Ideal for high-performance storage with faster read and write speeds.\
<br /><br /> • Virtio-BLK – Efficient for virtualized environments, offering direct block device access with lower overhead.\
<br /><br /> • Virtio-SCSI – Flexible and scalable, supporting advanced features like hot-swapping and multiple devices.'),

instance_protocol_placeholder: T('Instance Protocol'),
instance_protocol_tooltip: T('Select the protocol for the instance\'s network connection.'),
instance_port_placeholder: T('Instance Port'),
Expand Down
5 changes: 5 additions & 0 deletions src/app/interfaces/virtualization.interface.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { FormControl, FormGroup } from '@angular/forms';
import { NetworkInterfaceAliasType } from 'app/enums/network-interface.enum';
import {
DiskIoBus,
VirtualizationDeviceType,
VirtualizationGlobalState,
VirtualizationGpuType,
Expand Down Expand Up @@ -41,6 +42,7 @@ export interface VirtualizationInstance {
vnc_port: number | null;
vnc_password: string | null;
secure_boot: boolean;
root_disk_io_bus: DiskIoBus;
root_disk_size: number | null;
userns_idmap?: UserNsIdmap;
}
Expand All @@ -61,6 +63,7 @@ export interface CreateVirtualizationInstance {
* Value in GBs.
*/
root_disk_size?: number;
root_disk_io_bus?: DiskIoBus;
source_type?: VirtualizationSource;
environment?: Record<string, string>;
autostart?: boolean;
Expand Down Expand Up @@ -89,6 +92,7 @@ export interface UpdateVirtualizationInstance {
enable_vnc?: boolean;
vnc_port?: number | null;
secure_boot?: boolean;
root_disk_io_bus?: DiskIoBus;
vnc_password?: string | null;
root_disk_size?: number;
}
Expand All @@ -110,6 +114,7 @@ export interface VirtualizationDisk {
source: string | null;
destination: string | null;
product_id: string;
io_bus: DiskIoBus;
boot_priority?: number;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<h1 matDialogTitle>
{{ 'Increase Root Disk Size' | translate }}
{{ 'Change Root Disk Setup' | translate }}
</h1>

<form class="ix-form-container" [formGroup]="form" (submit)="onSubmit()">
Expand All @@ -10,6 +10,14 @@ <h1 matDialogTitle>
[required]="true"
></ix-input>

<ix-select
formControlName="root_disk_io_bus"
[label]="'Root Disk I/O Bus' | translate"
[tooltip]="containersHelptext.io_bus_tooltip | translate"
[required]="true"
[options]="diskIoBusOptions$"
></ix-select>

<ix-form-actions>
<button
mat-button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,22 @@ import { of } from 'rxjs';
import { GiB } from 'app/constants/bytes.constant';
import { fakeSuccessfulJob } from 'app/core/testing/utils/fake-job.utils';
import { mockApi, mockJob } from 'app/core/testing/utils/mock-api.utils';
import { DiskIoBus } from 'app/enums/virtualization.enum';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { IxFormHarness } from 'app/modules/forms/ix-forms/testing/ix-form.harness';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { ApiService } from 'app/modules/websocket/api.service';
import {
IncreaseRootDiskSizeComponent,
} from 'app/pages/instances/components/all-instances/instance-details/instance-disks/increase-root-disk-size/increase-root-disk-size.component';
ChangeRootDiskSetupComponent,
} from 'app/pages/instances/components/all-instances/instance-details/instance-disks/change-root-disk-setup/change-root-disk-setup.component';

describe('IncreaseRootDiskSizeComponent', () => {
let spectator: Spectator<IncreaseRootDiskSizeComponent>;
describe('ChangeRootDiskSetupComponent', () => {
let spectator: Spectator<ChangeRootDiskSetupComponent>;
let loader: HarnessLoader;

const createComponent = createComponentFactory({
component: IncreaseRootDiskSizeComponent,
component: ChangeRootDiskSetupComponent,
providers: [
mockApi([
mockJob('virt.instance.update', fakeSuccessfulJob()),
Expand All @@ -38,6 +39,7 @@ describe('IncreaseRootDiskSizeComponent', () => {
useValue: {
id: 'test',
root_disk_size: 2 * GiB,
root_disk_io_bus: DiskIoBus.VirtioBlk,
} as VirtualizationInstance,
},
],
Expand All @@ -53,21 +55,23 @@ describe('IncreaseRootDiskSizeComponent', () => {

expect(await form.getValues()).toEqual({
'Root Disk Size (in GiB)': '2',
'Root Disk I/O Bus': 'Virtio-BLK',
});
});

it('increases root disk size when new value is set', async () => {
const form = await loader.getHarness(IxFormHarness);
await form.fillForm({
'Root Disk Size (in GiB)': '4',
'Root Disk I/O Bus': 'NVMe',
});

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

expect(spectator.inject(ApiService).job).toHaveBeenCalledWith('virt.instance.update', [
'test',
{ root_disk_size: 4 },
{ root_disk_size: 4, root_disk_io_bus: DiskIoBus.Nvme },
]);
expect(spectator.inject(DialogService).jobDialog).toHaveBeenCalled();
expect(spectator.inject(SnackbarService).success).toHaveBeenCalled();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,16 @@ import {
} from '@angular/material/dialog';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { of } from 'rxjs';
import { GiB } from 'app/constants/bytes.constant';
import { DiskIoBus, diskIoBusLabels } from 'app/enums/virtualization.enum';
import { mapToOptions } from 'app/helpers/options.helper';
import { containersHelptext } from 'app/helptext/virtualization/containers';
import { VirtualizationInstance } from 'app/interfaces/virtualization.interface';
import { DialogService } from 'app/modules/dialog/dialog.service';
import { FormActionsComponent } from 'app/modules/forms/ix-forms/components/form-actions/form-actions.component';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
import { IxFormatterService } from 'app/modules/forms/ix-forms/services/ix-formatter.service';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
Expand All @@ -21,9 +26,9 @@ import { ErrorHandlerService } from 'app/services/error-handler.service';

@UntilDestroy()
@Component({
selector: 'ix-increase-root-disk-size',
templateUrl: './increase-root-disk-size.component.html',
styleUrls: ['./increase-root-disk-size.component.scss'],
selector: 'ix-change-root-disk-setup',
templateUrl: './change-root-disk-setup.component.html',
styleUrls: ['./change-root-disk-setup.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
Expand All @@ -34,14 +39,19 @@ import { ErrorHandlerService } from 'app/services/error-handler.service';
ReactiveFormsModule,
TestDirective,
TranslateModule,
IxSelectComponent,
FormActionsComponent,
],
})
export class IncreaseRootDiskSizeComponent {
export class ChangeRootDiskSetupComponent {
protected readonly form = this.formBuilder.group({
size: [0],
root_disk_io_bus: [DiskIoBus.Nvme],
});

protected readonly diskIoBusOptions$ = of(mapToOptions(diskIoBusLabels, this.translate));
protected readonly containersHelptext = containersHelptext;

constructor(
@Inject(MAT_DIALOG_DATA) private instance: VirtualizationInstance,
private formBuilder: NonNullableFormBuilder,
Expand All @@ -50,19 +60,38 @@ export class IncreaseRootDiskSizeComponent {
private api: ApiService,
private translate: TranslateService,
private snackbar: SnackbarService,
private dialogRef: MatDialogRef<IncreaseRootDiskSizeComponent>,
private dialogRef: MatDialogRef<ChangeRootDiskSetupComponent>,
protected formatter: IxFormatterService,
) {
this.form.setValue({
size: this.instance.root_disk_size / GiB,
size: Number(this.instance.root_disk_size) / GiB,
root_disk_io_bus: this.instance.root_disk_io_bus,
});

this.form.controls.size.addValidators(Validators.min(this.instance.root_disk_size / GiB));
}

onSubmit(): void {
const payload = {
root_disk_size: this.form.value.size,
root_disk_io_bus: this.form.value.root_disk_io_bus,
};

if (payload.root_disk_size === Number(this.instance.root_disk_size) / GiB) {
delete payload.root_disk_size;
}

if (payload.root_disk_io_bus === this.instance.root_disk_io_bus) {
delete payload.root_disk_io_bus;
}

if (!Object.keys(payload).length) {
this.dialogRef.close();
return;
}

this.dialogService.jobDialog(
this.api.job('virt.instance.update', [this.instance.id, { root_disk_size: this.form.value.size }]),
this.api.job('virt.instance.update', [this.instance.id, payload]),
{ title: this.translate.instant('Increasing disk size') },
)
.afterClosed()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
[canCreateDataset]="isNew()"
></ix-explorer>

@if (form.controls.io_bus.enabled) {
<ix-select
formControlName="io_bus"
[label]="'I/O Bus' | translate"
[required]="true"
[options]="diskIoBusOptions$"
></ix-select>
}

@if (form.controls.destination.enabled) {
<ix-input
formControlName="destination"
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, VirtualizationType } from 'app/enums/virtualization.enum';
import { DiskIoBus, 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 @@ -133,4 +133,41 @@ describe('InstanceDiskFormComponent', () => {
});
});
});

describe('handling vm', () => {
beforeEach(() => {
spectator = createComponent({
providers: [
mockProvider(SlideInRef, {
getData: () => ({
instance: { id: 'my-instance', type: VirtualizationType.Vm },
}),
close: jest.fn(),
requireConfirmationWhen: jest.fn(),
}),
],
});
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('creates a new disk with io_bus option', async () => {
const form = await loader.getHarness(IxFormHarness);

await form.fillForm({ Source: '/mnt/path', 'I/O Bus': 'Virtio-BLK' });

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

expect(spectator.inject(SlideInRef).close).toHaveBeenCalledWith({
response: true,
error: false,
});
expect(spectator.inject(SnackbarService).success).toHaveBeenCalled();
expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('virt.instance.device_add', ['my-instance', {
source: '/mnt/path',
dev_type: VirtualizationDeviceType.Disk,
io_bus: DiskIoBus.VirtioBlk,
}]);
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ 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, VirtualizationType } from 'app/enums/virtualization.enum';
import { diskIoBusLabels, VirtualizationDeviceType, VirtualizationType } from 'app/enums/virtualization.enum';
import { mapToOptions } from 'app/helpers/options.helper';
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';
import { IxInputComponent } from 'app/modules/forms/ix-forms/components/ix-input/ix-input.component';
import { IxSelectComponent } from 'app/modules/forms/ix-forms/components/ix-select/ix-select.component';
import { FormErrorHandlerService } from 'app/modules/forms/ix-forms/services/form-error-handler.service';
import { ModalHeaderComponent } from 'app/modules/slide-ins/components/modal-header/modal-header.component';
import { SlideInRef } from 'app/modules/slide-ins/slide-in-ref';
Expand Down Expand Up @@ -43,16 +45,18 @@ interface InstanceDiskFormOptions {
IxFieldsetComponent,
FormActionsComponent,
MatButton,
IxSelectComponent,
TestDirective,
],
})
export class InstanceDiskFormComponent implements OnInit {
private existingDisk = signal<VirtualizationDisk | null>(null);

protected readonly diskIoBusOptions$ = of(mapToOptions(diskIoBusLabels, this.translate));
protected readonly isLoading = signal(false);

readonly directoryNodeProvider = computed(() => {
if (this.instance.type === VirtualizationType.Vm) {
if (this.isVm) {
return this.filesystem.getFilesystemNodeProvider({ zvolsOnly: true });
}

Expand All @@ -62,6 +66,7 @@ export class InstanceDiskFormComponent implements OnInit {
protected form = this.formBuilder.nonNullable.group({
source: ['', Validators.required],
destination: ['', Validators.required],
io_bus: ['', Validators.required],
});

protected isNew = computed(() => !this.existingDisk());
Expand All @@ -74,6 +79,10 @@ export class InstanceDiskFormComponent implements OnInit {
return this.slideInRef.getData().instance;
}

protected get isVm(): boolean {
return this.instance.type === VirtualizationType.Vm;
}

constructor(
private formBuilder: FormBuilder,
private errorHandler: FormErrorHandlerService,
Expand All @@ -95,10 +104,13 @@ export class InstanceDiskFormComponent implements OnInit {
this.form.patchValue({
source: disk.source || '',
destination: disk.destination || '',
io_bus: disk.io_bus || null,
});
}
if (this.instance.type === VirtualizationType.Vm) {
if (this.isVm) {
this.form.controls.destination.disable();
} else {
this.form.controls.io_bus.disable();
}
}

Expand Down
Loading

0 comments on commit 3885332

Please sign in to comment.