Skip to content

Commit

Permalink
NAS-132542 / 25.04 / Allow devices to be added to containers (#11046)
Browse files Browse the repository at this point in the history
  • Loading branch information
undsoft authored Nov 15, 2024
1 parent 35ff82e commit 021b858
Show file tree
Hide file tree
Showing 111 changed files with 1,516 additions and 41 deletions.
22 changes: 22 additions & 0 deletions src/app/enums/role.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ export enum Role {
SharingIscsiTargetRead = 'SHARING_ISCSI_TARGET_READ',
SharingIscsiTargetWrite = 'SHARING_ISCSI_TARGET_WRITE',
SharingIscsiWrite = 'SHARING_ISCSI_WRITE',
SharingFtpRead = 'SHARING_FTP_READ',
SharingFtpWrite = 'SHARING_FTP_WRITE',
SharingAdmin = 'SHARING_ADMIN',
SharingNfsRead = 'SHARING_NFS_READ',
SharingNfsWrite = 'SHARING_NFS_WRITE',
Expand All @@ -80,6 +82,10 @@ export enum Role {
SupportWrite = 'SUPPORT_WRITE',
SystemAuditRead = 'SYSTEM_AUDIT_READ',
SystemAuditWrite = 'SYSTEM_AUDIT_WRITE',
SystemGeneralRead = 'SYSTEM_GENERAL_READ',
SystemGeneralWrite = 'SYSTEM_GENERAL_WRITE',
SystemAdvancedRead = 'SYSTEM_ADVANCED_READ',
SystemAdvancedWrite = 'SYSTEM_ADVANCED_WRITE',
AppsRead = 'APPS_READ',
AppsWrite = 'APPS_WRITE',
CatalogRead = 'CATALOG_READ',
Expand All @@ -98,7 +104,11 @@ export enum Role {
JbofWrite = 'JBOF_WRITE',
PoolScrubRead = 'POOL_SCRUB_READ',
PoolScrubWrite = 'POOL_SCRUB_WRITE',
VirtImageRead = 'VIRT_IMAGE_READ',
VirtImageWrite = 'VIRT_IMAGE_WRITE',
VirtInstanceRead = 'VIRT_INSTANCE_READ',
VirtInstanceWrite = 'VIRT_INSTANCE_WRITE',
VirtInstanceDelete = 'VIRT_INSTANCE_DELETE',
VirtGlobalRead = 'VIRT_GLOBAL_READ',
VirtGlobalWrite = 'VIRT_GLOBAL_WRITE',
VmDeviceRead = 'VM_DEVICE_READ',
Expand All @@ -111,6 +121,8 @@ export enum Role {
}

export const roleNames = new Map<Role, string>([
[Role.ApiKeyRead, T('API Key Read')],
[Role.ApiKeyWrite, T('API Key Write')],
[Role.TrueCommandRead, T('TrueCommand Read')],
[Role.TrueCommandWrite, T('TrueCommand Write')],
[Role.AlertListRead, T('Alert List Read')],
Expand Down Expand Up @@ -160,6 +172,8 @@ export const roleNames = new Map<Role, string>([
[Role.SharingIscsiTargetRead, T('Sharing iSCSI Target Read')],
[Role.SharingIscsiTargetWrite, T('Sharing iSCSI Target Write')],
[Role.SharingIscsiWrite, T('Sharing iSCSI Write')],
[Role.SharingFtpRead, T('Sharing FTP Read')],
[Role.SharingFtpWrite, T('Sharing FTP Write')],
[Role.SharingAdmin, T('Sharing Admin')],
[Role.SharingNfsRead, T('Sharing NFS Read')],
[Role.SharingNfsWrite, T('Sharing NFS Write')],
Expand Down Expand Up @@ -187,6 +201,10 @@ export const roleNames = new Map<Role, string>([
[Role.ReportingWrite, T('Reporting Write')],
[Role.ServiceRead, T('Service Read')],
[Role.ServiceWrite, T('Service Write')],
[Role.SystemGeneralRead, T('System General Read')],
[Role.SystemGeneralWrite, T('System General Write')],
[Role.SystemAdvancedRead, T('System Advanced Read')],
[Role.SystemAdvancedWrite, T('System Advanced Write')],
[Role.SupportRead, T('Support Read')],
[Role.SupportWrite, T('Support Write')],
[Role.SystemAuditRead, T('System Audit Read')],
Expand All @@ -209,7 +227,11 @@ export const roleNames = new Map<Role, string>([
[Role.JbofWrite, T('JBOF Write')],
[Role.PoolScrubRead, T('Pool Scrub Read')],
[Role.PoolScrubWrite, T('Pool Scrub Write')],
[Role.VirtImageRead, T('Virtualization Image Read')],
[Role.VirtImageWrite, T('Virtualization Image Write')],
[Role.VirtInstanceRead, T('Virtualization Instance Read')],
[Role.VirtInstanceWrite, T('Virtualization Instance Write')],
[Role.VirtInstanceDelete, T('Virtualization Instance Delete')],
[Role.VirtGlobalWrite, T('Virtualization Global Write')],
[Role.VirtGlobalRead, T('Virtualization Global Read')],
[Role.VmDeviceRead, T('VM Device Read')],
Expand Down
6 changes: 6 additions & 0 deletions src/app/interfaces/virtualization.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export type VirtualizationDevice =

export interface VirtualizationDisk {
name: string;
description: string;
dev_type: VirtualizationDeviceType.Disk;
readonly: boolean;
source: string | null;
Expand All @@ -68,6 +69,7 @@ export interface VirtualizationDisk {

export interface VirtualizationGpu {
name: string;
description: string;
readonly: boolean;
dev_type: VirtualizationDeviceType.Gpu;
gpu_type: VirtualizationGpuType;
Expand All @@ -84,6 +86,7 @@ export interface VirtualizationGpu {

export interface VirtualizationProxy {
name: string;
description: string;
dev_type: VirtualizationDeviceType.Proxy;
readonly: boolean;
source_proto: VirtualizationProxyProtocol;
Expand All @@ -94,13 +97,15 @@ export interface VirtualizationProxy {

export interface VirtualizationNic {
name: string;
description: string;
dev_type: VirtualizationDeviceType.Nic;
readonly: boolean;
network: string;
}

export interface VirtualizationTpm {
name: string;
description: string;
dev_type: VirtualizationDeviceType.Tpm;
readonly: boolean;
path: string;
Expand All @@ -109,6 +114,7 @@ export interface VirtualizationTpm {

export interface VirtualizationUsb {
name: string;
description: string;
dev_type: VirtualizationDeviceType.Usb;
readonly: boolean;
bus: number;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
@if (isLoadingDevices()) {
<ngx-skeleton-loader class="loader"></ngx-skeleton-loader>
} @else if (hasDevicesToAdd()) {
<button
mat-button
ixTest="add-device"
[disabled]=""
[matMenuTriggerFor]="addDeviceMenu"
>
{{ 'Add' | translate }}
</button>

<mat-menu #addDeviceMenu="matMenu">
@if (availableUsbDevices().length) {
<h4 class="menu-header">{{ 'USB Devices' | translate }}</h4>
@for (usb of availableUsbDevices(); track usb.product_id) {
<button
mat-menu-item
[ixTest]="['add-usb-device', usb.product]"
(click)="addUsb(usb)"
>
{{ usb.product }}
</button>
}
}

@if (availableGpuDevices().length) {
<h4 class="menu-header">{{ 'GPUs' | translate }}</h4>
@for (gpu of availableGpuDevices(); track gpu) {
<button
mat-menu-item
[ixTest]="['add-gpu-device', gpu.description]"
(click)="addGpu(gpu)"
>
{{ gpu.description }}
</button>
}
}
</mat-menu>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.menu-header {
border-bottom: 1px solid var(--lines);
font-size: 12px;
margin-top: 12px;
padding-bottom: 6px;
padding-left: 15px;
}

:host .loader {
display: block;
flex: unset;
width: 80px;

::ng-deep .loader {
margin-bottom: -6px;
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { HarnessLoader } from '@angular/cdk/testing';
import { TestbedHarnessEnvironment } from '@angular/cdk/testing/testbed';
import { MatMenuHarness } from '@angular/material/menu/testing';
import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest';
import { mockApi, mockCall } from 'app/core/testing/utils/mock-api.utils';
import { VirtualizationDeviceType } from 'app/enums/virtualization.enum';
import { AvailableGpu, AvailableUsb, VirtualizationDevice } from 'app/interfaces/virtualization.interface';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import {
AddDeviceMenuComponent,
} from 'app/pages/virtualization/components/all-instances/instance-details/instance-devices/add-device-menu/add-device-menu.component';
import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/virtualization-instances.store';
import { ApiService } from 'app/services/api.service';

describe('AddDeviceMenuComponent', () => {
let spectator: Spectator<AddDeviceMenuComponent>;
let loader: HarnessLoader;
const createComponent = createComponentFactory({
component: AddDeviceMenuComponent,
providers: [
mockApi([
mockCall('virt.device.usb_choices', {
usb1: {
product_id: 'already-added',
product: 'Web Cam',
} as AvailableUsb,
usb2: {
product_id: 'new',
product: 'Card Reader',
} as AvailableUsb,
}),
mockCall('virt.device.gpu_choices', {
gpu1: {
description: 'NDIVIA XTR 2000',
} as AvailableGpu,
gpu2: {
description: 'MAD Galeon 5000',
} as AvailableGpu,
}),
mockCall('virt.instance.device_add'),
]),
mockProvider(VirtualizationInstancesStore, {
selectedInstance: () => ({ id: 'my-instance' }),
selectedInstanceDevices: () => [
{
dev_type: VirtualizationDeviceType.Usb,
product_id: 'already-added',
},
{
dev_type: VirtualizationDeviceType.Gpu,
description: 'NDIVIA XTR 2000',
},
] as VirtualizationDevice[],
loadDevices: jest.fn(),
isLoadingDevices: () => false,
}),
mockProvider(SnackbarService),
],
});

beforeEach(() => {
spectator = createComponent();
loader = TestbedHarnessEnvironment.loader(spectator.fixture);
});

it('shows available USB devices and GPUs that have not been already added to this system', async () => {
const menu = await loader.getHarness(MatMenuHarness.with({ triggerText: 'Add' }));
await menu.open();

const menuItems = await menu.getItems();
expect(menuItems).toHaveLength(2);
expect(await menuItems[0].getText()).toContain('Card Reader');
expect(await menuItems[1].getText()).toContain('MAD Galeon 5000');
});

it('adds a usb device when it is selected', async () => {
const menu = await loader.getHarness(MatMenuHarness.with({ triggerText: 'Add' }));
await menu.open();

await menu.clickItem({ text: 'Card Reader' });

expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('virt.instance.device_add', ['my-instance', {
dev_type: VirtualizationDeviceType.Usb,
product_id: 'new',
} as VirtualizationDevice]);
expect(spectator.inject(VirtualizationInstancesStore).loadDevices).toHaveBeenCalled();
expect(spectator.inject(SnackbarService).success).toHaveBeenCalledWith('Device was added');
});

it('adds a gpu when it is selected', async () => {
const menu = await loader.getHarness(MatMenuHarness.with({ triggerText: 'Add' }));
await menu.open();

await menu.clickItem({ text: 'MAD Galeon 5000' });

expect(spectator.inject(ApiService).call).toHaveBeenCalledWith('virt.instance.device_add', ['my-instance', {
dev_type: VirtualizationDeviceType.Gpu,
description: 'MAD Galeon 5000',
} as VirtualizationDevice]);
expect(spectator.inject(VirtualizationInstancesStore).loadDevices).toHaveBeenCalled();
expect(spectator.inject(SnackbarService).success).toHaveBeenCalledWith('Device was added');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import { ChangeDetectionStrategy, Component, computed } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { MatButton } from '@angular/material/button';
import { MatMenu, MatMenuItem, MatMenuTrigger } from '@angular/material/menu';
import { UntilDestroy, untilDestroyed } from '@ngneat/until-destroy';
import { TranslateModule, TranslateService } from '@ngx-translate/core';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { VirtualizationDeviceType, VirtualizationGpuType, VirtualizationType } from 'app/enums/virtualization.enum';
import {
AvailableGpu,
AvailableUsb,
VirtualizationDevice,
VirtualizationGpu,
VirtualizationUsb,
} from 'app/interfaces/virtualization.interface';
import { AppLoaderService } from 'app/modules/loader/app-loader.service';
import { SnackbarService } from 'app/modules/snackbar/services/snackbar.service';
import { TestDirective } from 'app/modules/test-id/test.directive';
import { VirtualizationInstancesStore } from 'app/pages/virtualization/stores/virtualization-instances.store';
import { ApiService } from 'app/services/api.service';
import { ErrorHandlerService } from 'app/services/error-handler.service';

@UntilDestroy()
@Component({
selector: 'ix-add-device-menu',
templateUrl: './add-device-menu.component.html',
styleUrls: ['./add-device-menu.component.scss'],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
MatButton,
MatMenu,
MatMenuItem,
TestDirective,
TranslateModule,
MatMenuTrigger,
NgxSkeletonLoaderModule,
],
})
export class AddDeviceMenuComponent {
private readonly usbChoices = toSignal(this.api.call('virt.device.usb_choices'), { initialValue: {} });
// TODO: Stop hardcoding params
private readonly gpuChoices = toSignal(this.api.call('virt.device.gpu_choices', [VirtualizationType.Container, VirtualizationGpuType.Physical]), { initialValue: {} });

protected readonly isLoadingDevices = this.instanceStore.isLoadingDevices;

protected readonly availableUsbDevices = computed(() => {
const usbChoices = Object.values(this.usbChoices());
const existingUsbDevices = this.instanceStore.selectedInstanceDevices()
.filter((device) => device.dev_type === VirtualizationDeviceType.Usb);

return usbChoices.filter((usb) => {
return !existingUsbDevices.find((device) => device.product_id === usb.product_id);
});
});

protected readonly availableGpuDevices = computed(() => {
const gpuChoices = Object.values(this.gpuChoices());
const existingGpuDevices = this.instanceStore.selectedInstanceDevices()
.filter((device) => device.dev_type === VirtualizationDeviceType.Gpu);

return gpuChoices.filter((gpu) => {
// TODO: Condition is incorrect.
return !existingGpuDevices.find((device) => device.description === gpu.description);
});
});

protected readonly hasDevicesToAdd = computed(() => {
return this.availableUsbDevices().length > 0 || this.availableGpuDevices().length > 0;
});

constructor(
private instanceStore: VirtualizationInstancesStore,
private api: ApiService,
private errorHandler: ErrorHandlerService,
private loader: AppLoaderService,
private snackbar: SnackbarService,
private translate: TranslateService,
) {}

protected addUsb(usb: AvailableUsb): void {
this.addDevice({
dev_type: VirtualizationDeviceType.Usb,
product_id: usb.product_id,
} as VirtualizationUsb);
}

protected addGpu(gpu: AvailableGpu): void {
this.addDevice({
dev_type: VirtualizationDeviceType.Gpu,
// TODO: Incorrect value.
description: gpu.description,
} as VirtualizationGpu);
}

private addDevice(payload: VirtualizationDevice): void {
const instanceId = this.instanceStore.selectedInstance().id;
this.api.call('virt.instance.device_add', [instanceId, payload])
.pipe(
this.loader.withLoader(),
this.errorHandler.catchError(),
untilDestroyed(this),
)
.subscribe(() => {
this.snackbar.success(this.translate.instant('Device was added'));
this.instanceStore.loadDevices();
});
}
}
Loading

0 comments on commit 021b858

Please sign in to comment.