import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
import { NvmeofGatewayComponent } from './nvmeof-gateway/nvmeof-gateway.component';
+import { NvmeofSubsystemsComponent } from './nvmeof-subsystems/nvmeof-subsystems.component';
+import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details/nvmeof-subsystems-details.component';
+import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
+import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-subsystems-form.component';
@NgModule({
imports: [
RbdConfigurationFormComponent,
RbdTabsComponent,
RbdPerformanceComponent,
- NvmeofGatewayComponent
+ NvmeofGatewayComponent,
+ NvmeofSubsystemsComponent,
+ NvmeofSubsystemsDetailsComponent,
+ NvmeofTabsComponent,
+ NvmeofSubsystemsFormComponent
],
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
})
}
},
children: [
- { path: '', redirectTo: 'gateways', pathMatch: 'full' },
+ { path: '', redirectTo: 'subsystems', pathMatch: 'full' },
+ {
+ path: 'subsystems',
+ component: NvmeofSubsystemsComponent,
+ data: { breadcrumbs: 'Subsystems' },
+ children: [
+ { path: '', component: NvmeofSubsystemsComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: NvmeofSubsystemsFormComponent,
+ outlet: 'modal'
+ },
+ {
+ path: `${URLVerbs.EDIT}/:subsystem_nqn`,
+ component: NvmeofSubsystemsFormComponent,
+ outlet: 'modal'
+ }
+ ]
+ },
{ path: 'gateways', component: NvmeofGatewayComponent, data: { breadcrumbs: 'Gateways' } }
]
}
-<ul class="nav nav-tabs">
- <li class="nav-item">
- <a class="nav-link"
- routerLink="/block/nvmeof/gateways"
- routerLinkActive="active"
- ariaCurrentWhenActive="page"
- i18n>Gateways</a>
- </li>
-</ul>
+<cd-nvmeof-tabs></cd-nvmeof-tabs>
<legend i18n>
Gateways
-import { Component, OnInit } from '@angular/core';
+import { Component } from '@angular/core';
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
-import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
import { NvmeofGateway } from '~/app/shared/models/nvmeof';
templateUrl: './nvmeof-gateway.component.html',
styleUrls: ['./nvmeof-gateway.component.scss']
})
-export class NvmeofGatewayComponent extends ListWithDetails implements OnInit {
+export class NvmeofGatewayComponent {
gateways: NvmeofGateway[] = [];
gatewayColumns: any;
selection = new CdTableSelection();
- constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) {
- super();
- }
+ constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) {}
ngOnInit() {
this.gatewayColumns = [
--- /dev/null
+<ng-container *ngIf="selection">
+ <nav ngbNav
+ #nav="ngbNav"
+ class="nav-tabs"
+ cdStatefulTab="subsystem-details">
+ <ng-container ngbNavItem="details">
+ <a ngbNavLink
+ i18n>Details</a>
+ <ng-template ngbNavContent>
+ <cd-table-key-value [data]="data">
+ </cd-table-key-value>
+ </ng-template>
+ </ng-container>
+ </nav>
+
+ <div [ngbNavOutlet]="nav"></div>
+</ng-container>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details.component';
+
+describe('NvmeofSubsystemsDetailsComponent', () => {
+ let component: NvmeofSubsystemsDetailsComponent;
+ let fixture: ComponentFixture<NvmeofSubsystemsDetailsComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NvmeofSubsystemsDetailsComponent],
+ imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule, NgbNavModule]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(NvmeofSubsystemsDetailsComponent);
+ component = fixture.componentInstance;
+ component.selection = {
+ serial_number: 'Ceph30487186726692',
+ model_number: 'Ceph bdev Controller',
+ min_cntlid: 1,
+ max_cntlid: 2040,
+ subtype: 'NVMe',
+ nqn: 'nqn.2001-07.com.ceph:1720603703820',
+ namespace_count: 1,
+ max_namespaces: 256
+ };
+ component.ngOnChanges();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should prepare data', () => {
+ expect(component.data).toEqual({
+ 'Serial Number': 'Ceph30487186726692',
+ 'Model Number': 'Ceph bdev Controller',
+ 'Minimum Controller Identifier': 1,
+ 'Maximum Controller Identifier': 2040,
+ 'Subsystem Type': 'NVMe'
+ });
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges } from '@angular/core';
+import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+
+@Component({
+ selector: 'cd-nvmeof-subsystems-details',
+ templateUrl: './nvmeof-subsystems-details.component.html',
+ styleUrls: ['./nvmeof-subsystems-details.component.scss']
+})
+export class NvmeofSubsystemsDetailsComponent implements OnChanges {
+ @Input()
+ selection: NvmeofSubsystem;
+
+ selectedItem: any;
+ data: any;
+
+ ngOnChanges() {
+ if (this.selection) {
+ this.selectedItem = this.selection;
+ this.data = {};
+ this.data[$localize`Serial Number`] = this.selectedItem.serial_number;
+ this.data[$localize`Model Number`] = this.selectedItem.model_number;
+ this.data[$localize`Minimum Controller Identifier`] = this.selectedItem.min_cntlid;
+ this.data[$localize`Maximum Controller Identifier`] = this.selectedItem.max_cntlid;
+ this.data[$localize`Subsystem Type`] = this.selectedItem.subtype;
+ }
+ }
+}
--- /dev/null
+<cd-modal [pageURL]="pageURL"
+ [modalRef]="activeModal">
+ <span class="modal-title"
+ i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
+ <ng-container class="modal-content">
+ <form name="subsystemForm"
+ #formDir="ngForm"
+ [formGroup]="subsystemForm"
+ novalidate>
+ <div class="modal-body">
+ <!-- NQN -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="nqn">
+ <span class="required"
+ i18n>NQN</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input name="nqn"
+ class="form-control"
+ type="text"
+ formControlName="nqn">
+ <cd-help-text>
+ The NVMe Qualified Name (NQN) is a unique and permanent name for the lifetime of the subsystem.
+ </cd-help-text>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('nqn', formDir, 'required')"
+ i18n>This field is required.</span>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('nqn', formDir, 'unique')"
+ i18n>This NQN is already in use.</span>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('nqn', formDir, 'pattern')"
+ i18n>An NQN should follow the format of<br/><<code>nqn.$year-$month.$reverseDomainName:$definedName</code>".></span>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('nqn', formDir, 'maxLength')"
+ i18n>An NQN should not be more than 223 bytes in length.</span>
+ </div>
+ </div>
+ <!-- Maximum Namespaces -->
+ <div class="form-group row">
+ <label class="cd-col-form-label"
+ for="max_namespaces">
+ <span i18n>Maximum Namespaces</span>
+ </label>
+ <div class="cd-col-form-input">
+ <input id="max_namespaces"
+ class="form-control"
+ type="text"
+ name="max_namespaces"
+ formControlName="max_namespaces">
+ <cd-help-text i18n>The maximum namespaces per subsystem. Default is 256.</cd-help-text>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('max_namespaces', formDir, 'min')"
+ i18n>The value must be at least 1.</span>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('max_namespaces', formDir, 'max')"
+ i18n>The value cannot be greated than 256.</span>
+ <span class="invalid-feedback"
+ *ngIf="subsystemForm.showError('max_namespaces', formDir, 'pattern')"
+ i18n>The value must be a positive integer.</span>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <div class="text-right">
+ <cd-form-button-panel (submitActionEvent)="onSubmit()"
+ [form]="subsystemForm"
+ [submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
+ </div>
+ </div>
+ </form>
+ </ng-container>
+</cd-modal>
--- /dev/null
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { ReactiveFormsModule } from '@angular/forms';
+import { RouterTestingModule } from '@angular/router/testing';
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { ToastrModule } from 'ngx-toastr';
+
+import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
+
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { SharedModule } from '~/app/shared/shared.module';
+import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form.component';
+import { FormHelper } from '~/testing/unit-test-helper';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+
+describe('NvmeofSubsystemsFormComponent', () => {
+ let component: NvmeofSubsystemsFormComponent;
+ let fixture: ComponentFixture<NvmeofSubsystemsFormComponent>;
+ let nvmeofService: NvmeofService;
+ let form: CdFormGroup;
+ let formHelper: FormHelper;
+ const mockTimestamp = 1720693470789;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NvmeofSubsystemsFormComponent],
+ providers: [NgbActiveModal],
+ imports: [
+ HttpClientTestingModule,
+ NgbTypeaheadModule,
+ ReactiveFormsModule,
+ RouterTestingModule,
+ SharedModule,
+ ToastrModule.forRoot()
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(NvmeofSubsystemsFormComponent);
+ component = fixture.componentInstance;
+ component.ngOnInit();
+ form = component.subsystemForm;
+ formHelper = new FormHelper(form);
+ spyOn(Date, 'now').and.returnValue(mockTimestamp);
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ describe('should test form', () => {
+ beforeEach(() => {
+ nvmeofService = TestBed.inject(NvmeofService);
+ spyOn(nvmeofService, 'createSubsystem').and.stub();
+ });
+
+ it('should be creating request correctly', () => {
+ const expectedNqn = 'nqn.2001-07.com.ceph:' + mockTimestamp;
+ component.onSubmit();
+ expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
+ nqn: expectedNqn,
+ max_namespaces: 256,
+ enable_ha: true
+ });
+ });
+
+ it('should give error on invalid nqn', () => {
+ formHelper.setValue('nqn', 'nqn:2001-07.com.ceph:');
+ component.onSubmit();
+ formHelper.expectError('nqn', 'pattern');
+ });
+
+ it('should give error on invalid max_namespaces', () => {
+ formHelper.setValue('max_namespaces', -56);
+ component.onSubmit();
+ formHelper.expectError('max_namespaces', 'pattern');
+ });
+
+ it('should give error on max_namespaces greater than 256', () => {
+ formHelper.setValue('max_namespaces', 300);
+ component.onSubmit();
+ formHelper.expectError('max_namespaces', 'max');
+ });
+
+ it('should give error on max_namespaces lesser than 1', () => {
+ formHelper.setValue('max_namespaces', 0);
+ component.onSubmit();
+ formHelper.expectError('max_namespaces', 'min');
+ });
+ });
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { UntypedFormControl, Validators } from '@angular/forms';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
+import { CdValidators } from '~/app/shared/forms/cd-validators';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { Router } from '@angular/router';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+
+@Component({
+ selector: 'cd-nvmeof-subsystems-form',
+ templateUrl: './nvmeof-subsystems-form.component.html',
+ styleUrls: ['./nvmeof-subsystems-form.component.scss']
+})
+export class NvmeofSubsystemsFormComponent implements OnInit {
+ permission: Permission;
+ subsystemForm: CdFormGroup;
+
+ action: string;
+ resource: string;
+ pageURL: string;
+
+ NQN_REGEX = /^nqn\.(19|20)\d\d-(0[1-9]|1[0-2])\.\D{2,3}(\.[A-Za-z0-9-]+)+(:[A-Za-z0-9-\.]+)$/;
+
+ constructor(
+ private authStorageService: AuthStorageService,
+ public actionLabels: ActionLabelsI18n,
+ public activeModal: NgbActiveModal,
+ private nvmeofService: NvmeofService,
+ private taskWrapperService: TaskWrapperService,
+ private router: Router
+ ) {
+ this.permission = this.authStorageService.getPermissions().nvmeof;
+ this.resource = $localize`Subsystem`;
+ this.pageURL = 'block/nvmeof/subsystems';
+ }
+
+ ngOnInit() {
+ this.createForm();
+ this.action = this.actionLabels.CREATE;
+ }
+
+ createForm() {
+ this.subsystemForm = new CdFormGroup({
+ nqn: new UntypedFormControl('nqn.2001-07.com.ceph:' + Date.now(), {
+ validators: [
+ Validators.required,
+ Validators.pattern(this.NQN_REGEX),
+ CdValidators.custom(
+ 'maxLength',
+ (nqnInput: string) => new TextEncoder().encode(nqnInput).length > 223
+ )
+ ],
+ asyncValidators: [
+ CdValidators.unique(this.nvmeofService.isSubsystemPresent, this.nvmeofService)
+ ]
+ }),
+ max_namespaces: new UntypedFormControl(256, {
+ validators: [CdValidators.number(false), Validators.max(256), Validators.min(1)]
+ })
+ });
+ }
+
+ onSubmit() {
+ const component = this;
+ const nqn: string = this.subsystemForm.getValue('nqn');
+ let max_namespaces: number = Number(this.subsystemForm.getValue('max_namespaces'));
+
+ const request = {
+ nqn,
+ max_namespaces,
+ enable_ha: true
+ };
+
+ if (!max_namespaces) {
+ delete request.max_namespaces;
+ }
+
+ let taskUrl = `nvmeof/subsystem/${URLVerbs.CREATE}`;
+
+ this.taskWrapperService
+ .wrapTaskAroundCall({
+ task: new FinishedTask(taskUrl, {
+ nqn: nqn
+ }),
+ call: this.nvmeofService.createSubsystem(request)
+ })
+ .subscribe({
+ error() {
+ component.subsystemForm.setErrors({ cdSubmitButton: true });
+ },
+ complete: () => {
+ this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
+ }
+ });
+ }
+}
--- /dev/null
+<cd-nvmeof-tabs></cd-nvmeof-tabs>
+<legend i18n>
+ Subsystems
+ <cd-help-text>
+ A subsystem presents a collection of controllers which are used to access namespaces.
+ </cd-help-text>
+</legend>
+<cd-table [data]="subsystems"
+ columnMode="flex"
+ (fetchData)="getSubsystems()"
+ [columns]="subsystemsColumns"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (updateSelection)="updateSelection($event)">
+
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions [permission]="permission"
+ [selection]="selection"
+ class="btn-group"
+ [tableActions]="tableActions">
+ </cd-table-actions>
+ </div>
+
+ <cd-nvmeof-subsystems-details cdTableDetail
+ [selection]="expandedRow">
+ </cd-nvmeof-subsystems-details>
+</cd-table>
+<router-outlet name="modal"></router-outlet>
--- /dev/null
+import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
+import { HttpClientModule } from '@angular/common/http';
+import { of } from 'rxjs';
+import { RouterTestingModule } from '@angular/router/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+
+import { NvmeofService } from '../../../shared/api/nvmeof.service';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { NvmeofSubsystemsComponent } from './nvmeof-subsystems.component';
+import { NvmeofTabsComponent } from '../nvmeof-tabs/nvmeof-tabs.component';
+import { NvmeofSubsystemsDetailsComponent } from '../nvmeof-subsystems-details/nvmeof-subsystems-details.component';
+
+const mockSubsystems = [
+ {
+ nqn: 'nqn.2001-07.com.ceph:1720603703820',
+ enable_ha: true,
+ serial_number: 'Ceph30487186726692',
+ model_number: 'Ceph bdev Controller',
+ min_cntlid: 1,
+ max_cntlid: 2040,
+ namespace_count: 0,
+ subtype: 'NVMe',
+ max_namespaces: 256
+ }
+];
+
+class MockNvmeOfService {
+ listSubsystems() {
+ return of(mockSubsystems);
+ }
+}
+
+class MockAuthStorageService {
+ getPermissions() {
+ return { nvmeof: {} };
+ }
+}
+
+class MockModalService {}
+
+class MockTaskWrapperService {}
+
+describe('NvmeofSubsystemsComponent', () => {
+ let component: NvmeofSubsystemsComponent;
+ let fixture: ComponentFixture<NvmeofSubsystemsComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [
+ NvmeofSubsystemsComponent,
+ NvmeofTabsComponent,
+ NvmeofSubsystemsDetailsComponent
+ ],
+ imports: [HttpClientModule, RouterTestingModule, SharedModule],
+ providers: [
+ { provide: NvmeofService, useClass: MockNvmeOfService },
+ { provide: AuthStorageService, useClass: MockAuthStorageService },
+ { provide: ModalService, useClass: MockModalService },
+ { provide: TaskWrapperService, useClass: MockTaskWrapperService }
+ ]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(NvmeofSubsystemsComponent);
+ component = fixture.componentInstance;
+ component.ngOnInit();
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should retrieve subsystems', fakeAsync(() => {
+ component.getSubsystems();
+ tick();
+ expect(component.subsystems).toEqual(mockSubsystems);
+ }));
+});
--- /dev/null
+import { Component, OnInit } from '@angular/core';
+import { Router } from '@angular/router';
+
+import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
+import { FinishedTask } from '~/app/shared/models/finished-task';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { NvmeofService } from '~/app/shared/api/nvmeof.service';
+
+const BASE_URL = 'block/nvmeof/subsystems';
+
+@Component({
+ selector: 'cd-nvmeof-subsystems',
+ templateUrl: './nvmeof-subsystems.component.html',
+ styleUrls: ['./nvmeof-subsystems.component.scss']
+})
+export class NvmeofSubsystemsComponent extends ListWithDetails implements OnInit {
+ subsystems: NvmeofSubsystem[] = [];
+ subsystemsColumns: any;
+ permission: Permission;
+ selection = new CdTableSelection();
+ tableActions: CdTableAction[];
+ subsystemDetails: any[];
+
+ constructor(
+ private nvmeofService: NvmeofService,
+ private authStorageService: AuthStorageService,
+ public actionLabels: ActionLabelsI18n,
+ private router: Router,
+ private modalService: ModalService,
+ private taskWrapper: TaskWrapperService
+ ) {
+ super();
+ this.permission = this.authStorageService.getPermissions().nvmeof;
+ }
+
+ ngOnInit() {
+ this.subsystemsColumns = [
+ {
+ name: $localize`NQN`,
+ prop: 'nqn'
+ },
+ {
+ name: $localize`# Namespaces`,
+ prop: 'namespace_count'
+ },
+ {
+ name: $localize`# Maximum Namespaces`,
+ prop: 'max_namespaces'
+ }
+ ];
+ this.tableActions = [
+ {
+ name: this.actionLabels.CREATE,
+ permission: 'create',
+ icon: Icons.add,
+ click: () => this.router.navigate([BASE_URL, { outlets: { modal: [URLVerbs.CREATE] } }]),
+ canBePrimary: (selection: CdTableSelection) => !selection.hasSelection
+ },
+ {
+ name: this.actionLabels.DELETE,
+ permission: 'delete',
+ icon: Icons.destroy,
+ click: () => this.deleteSubsystemModal()
+ }
+ ];
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ getSubsystems() {
+ this.nvmeofService
+ .listSubsystems()
+ .subscribe((subsystems: NvmeofSubsystem[] | NvmeofSubsystem) => {
+ if (Array.isArray(subsystems)) this.subsystems = subsystems;
+ else this.subsystems = [subsystems];
+ });
+ }
+
+ deleteSubsystemModal() {
+ const subsystem = this.selection.first();
+ this.modalService.show(CriticalConfirmationModalComponent, {
+ itemDescription: 'Subsystem',
+ itemNames: [subsystem.nqn],
+ actionDescription: 'delete',
+ submitActionObservable: () =>
+ this.taskWrapper.wrapTaskAroundCall({
+ task: new FinishedTask('nvmeof/subsystem/delete', { nqn: subsystem.nqn }),
+ call: this.nvmeofService.deleteSubsystem(subsystem.nqn)
+ })
+ });
+ }
+}
--- /dev/null
+<ul class="nav nav-tabs">
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/nvmeof/subsystems"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ i18n>Subsystems</a>
+ </li>
+ <li class="nav-item">
+ <a class="nav-link"
+ routerLink="/block/nvmeof/gateways"
+ routerLinkActive="active"
+ ariaCurrentWhenActive="page"
+ i18n>Gateways</a>
+ </li>
+</ul>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { NvmeofTabsComponent } from './nvmeof-tabs.component';
+
+describe('NvmeofTabsComponent', () => {
+ let component: NvmeofTabsComponent;
+ let fixture: ComponentFixture<NvmeofTabsComponent>;
+
+ beforeEach(async () => {
+ await TestBed.configureTestingModule({
+ declarations: [NvmeofTabsComponent]
+ }).compileComponents();
+
+ fixture = TestBed.createComponent(NvmeofTabsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component } from '@angular/core';
+
+@Component({
+ selector: 'cd-nvmeof-tabs',
+ templateUrl: './nvmeof-tabs.component.html',
+ styleUrls: ['./nvmeof-tabs.component.scss']
+})
+export class NvmeofTabsComponent {}
-import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
+import { HttpClient } from '@angular/common/http';
+
+import _ from 'lodash';
+import { Observable, of as observableOf } from 'rxjs';
+import { catchError, mapTo } from 'rxjs/operators';
const BASE_URL = 'api/nvmeof';
listGateways() {
return this.http.get(`${BASE_URL}/gateway`);
}
+
+ listSubsystems() {
+ return this.http.get(`${BASE_URL}/subsystem`);
+ }
+
+ getSubsystem(subsystemNQN: string) {
+ return this.http.get(`${BASE_URL}/subsystem/${subsystemNQN}`);
+ }
+
+ createSubsystem(request: { nqn: string; max_namespaces?: number; enable_ha: boolean }) {
+ return this.http.post(`${BASE_URL}/subsystem`, request, { observe: 'response' });
+ }
+
+ deleteSubsystem(subsystemNQN: string) {
+ return this.http.delete(`${BASE_URL}/subsystem/${subsystemNQN}`, {
+ observe: 'response'
+ });
+ }
+
+ isSubsystemPresent(subsystemNqn: string): Observable<boolean> {
+ return this.getSubsystem(subsystemNqn).pipe(
+ mapTo(true),
+ catchError((e) => {
+ e?.preventDefault();
+ return observableOf(false);
+ })
+ );
+ }
}
load_balancing_group: string;
spdk_version: string;
}
+
+export interface NvmeofSubsystem {
+ nqn: string;
+ serial_number: string;
+ model_number: string;
+ min_cntlid: number;
+ max_cntlid: number;
+ namespace_count: number;
+ subtype: string;
+ max_namespaces: number;
+}
'iscsi/target/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
this.iscsiTarget(metadata)
),
+ // NVME/TCP tasks
+ 'nvmeof/subsystem/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
+ this.nvmeofSubsystem(metadata)
+ ),
+ 'nvmeof/subsystem/delete': this.newTaskMessage(this.commonOperations.delete, (metadata) =>
+ this.nvmeofSubsystem(metadata)
+ ),
'nfs/create': this.newTaskMessage(this.commonOperations.create, (metadata) =>
this.nfs(metadata)
),
return $localize`target '${metadata.target_iqn}'`;
}
+ nvmeofSubsystem(metadata: any) {
+ return $localize`subsystem '${metadata.nqn}'`;
+ }
+
nfs(metadata: any) {
return $localize`NFS '${metadata.cluster_id}\:${
metadata.export_id ? metadata.export_id : metadata.path