Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

NAS-134393 / 25.04.0 / Expose IO bus configuration in the UI (by AlexKarpov98) #11671

Merged
merged 2 commits into from
Mar 6, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading