Skip to content

Commit b85f982

Browse files
committed
mgr/dashboard: Configure subsystems from dashboard
Fixes https://tracker.ceph.com/issues/66659 - adds subsytems tab - adds subsystem listing view - adds create subsystem modal - adds delete subsystem - adds unit tests Signed-off-by: Afreen Misbah <[email protected]>
1 parent 6052bfa commit b85f982

22 files changed

+705
-18
lines changed

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/block.module.ts

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-mov
3939
import { RbdTrashPurgeModalComponent } from './rbd-trash-purge-modal/rbd-trash-purge-modal.component';
4040
import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-trash-restore-modal.component';
4141
import { NvmeofGatewayComponent } from './nvmeof-gateway/nvmeof-gateway.component';
42+
import { NvmeofSubsystemsComponent } from './nvmeof-subsystems/nvmeof-subsystems.component';
43+
import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details/nvmeof-subsystems-details.component';
44+
import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
45+
import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-subsystems-form.component';
4246

4347
@NgModule({
4448
imports: [
@@ -79,7 +83,11 @@ import { NvmeofGatewayComponent } from './nvmeof-gateway/nvmeof-gateway.componen
7983
RbdConfigurationFormComponent,
8084
RbdTabsComponent,
8185
RbdPerformanceComponent,
82-
NvmeofGatewayComponent
86+
NvmeofGatewayComponent,
87+
NvmeofSubsystemsComponent,
88+
NvmeofSubsystemsDetailsComponent,
89+
NvmeofTabsComponent,
90+
NvmeofSubsystemsFormComponent
8391
],
8492
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
8593
})
@@ -220,7 +228,25 @@ const routes: Routes = [
220228
}
221229
},
222230
children: [
223-
{ path: '', redirectTo: 'gateways', pathMatch: 'full' },
231+
{ path: '', redirectTo: 'subsystems', pathMatch: 'full' },
232+
{
233+
path: 'subsystems',
234+
component: NvmeofSubsystemsComponent,
235+
data: { breadcrumbs: 'Subsystems' },
236+
children: [
237+
{ path: '', component: NvmeofSubsystemsComponent },
238+
{
239+
path: URLVerbs.CREATE,
240+
component: NvmeofSubsystemsFormComponent,
241+
outlet: 'modal'
242+
},
243+
{
244+
path: `${URLVerbs.EDIT}/:subsystem_nqn`,
245+
component: NvmeofSubsystemsFormComponent,
246+
outlet: 'modal'
247+
}
248+
]
249+
},
224250
{ path: 'gateways', component: NvmeofGatewayComponent, data: { breadcrumbs: 'Gateways' } }
225251
]
226252
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.html

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,4 @@
1-
<ul class="nav nav-tabs">
2-
<li class="nav-item">
3-
<a class="nav-link"
4-
routerLink="/block/nvmeof/gateways"
5-
routerLinkActive="active"
6-
ariaCurrentWhenActive="page"
7-
i18n>Gateways</a>
8-
</li>
9-
</ul>
1+
<cd-nvmeof-tabs></cd-nvmeof-tabs>
102

113
<legend i18n>
124
Gateways

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-gateway/nvmeof-gateway.component.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component } from '@angular/core';
22

33
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
4-
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
54
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
65
import { NvmeofGateway } from '~/app/shared/models/nvmeof';
76

@@ -12,14 +11,12 @@ import { NvmeofService } from '../../../shared/api/nvmeof.service';
1211
templateUrl: './nvmeof-gateway.component.html',
1312
styleUrls: ['./nvmeof-gateway.component.scss']
1413
})
15-
export class NvmeofGatewayComponent extends ListWithDetails implements OnInit {
14+
export class NvmeofGatewayComponent {
1615
gateways: NvmeofGateway[] = [];
1716
gatewayColumns: any;
1817
selection = new CdTableSelection();
1918

20-
constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) {
21-
super();
22-
}
19+
constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) {}
2320

2421
ngOnInit() {
2522
this.gatewayColumns = [
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<ng-container *ngIf="selection">
2+
<nav ngbNav
3+
#nav="ngbNav"
4+
class="nav-tabs"
5+
cdStatefulTab="subsystem-details">
6+
<ng-container ngbNavItem="details">
7+
<a ngbNavLink
8+
i18n>Details</a>
9+
<ng-template ngbNavContent>
10+
<cd-table-key-value [data]="data">
11+
</cd-table-key-value>
12+
</ng-template>
13+
</ng-container>
14+
</nav>
15+
16+
<div [ngbNavOutlet]="nav"></div>
17+
</ng-container>

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-details/nvmeof-subsystems-details.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { HttpClientTestingModule } from '@angular/common/http/testing';
2+
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
3+
import { ComponentFixture, TestBed } from '@angular/core/testing';
4+
5+
import { NgbNavModule } from '@ng-bootstrap/ng-bootstrap';
6+
7+
import { SharedModule } from '~/app/shared/shared.module';
8+
import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details.component';
9+
10+
describe('NvmeofSubsystemsDetailsComponent', () => {
11+
let component: NvmeofSubsystemsDetailsComponent;
12+
let fixture: ComponentFixture<NvmeofSubsystemsDetailsComponent>;
13+
14+
beforeEach(async () => {
15+
await TestBed.configureTestingModule({
16+
declarations: [NvmeofSubsystemsDetailsComponent],
17+
imports: [BrowserAnimationsModule, SharedModule, HttpClientTestingModule, NgbNavModule]
18+
}).compileComponents();
19+
20+
fixture = TestBed.createComponent(NvmeofSubsystemsDetailsComponent);
21+
component = fixture.componentInstance;
22+
component.selection = {
23+
serial_number: 'Ceph30487186726692',
24+
model_number: 'Ceph bdev Controller',
25+
min_cntlid: 1,
26+
max_cntlid: 2040,
27+
subtype: 'NVMe',
28+
nqn: 'nqn.2001-07.com.ceph:1720603703820',
29+
namespace_count: 1,
30+
max_namespaces: 256
31+
};
32+
component.ngOnChanges();
33+
fixture.detectChanges();
34+
});
35+
36+
it('should create', () => {
37+
expect(component).toBeTruthy();
38+
});
39+
40+
it('should prepare data', () => {
41+
expect(component.data).toEqual({
42+
'Serial Number': 'Ceph30487186726692',
43+
'Model Number': 'Ceph bdev Controller',
44+
'Minimum Controller Identifier': 1,
45+
'Maximum Controller Identifier': 2040,
46+
'Subsystem Type': 'NVMe'
47+
});
48+
});
49+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Component, Input, OnChanges } from '@angular/core';
2+
import { NvmeofSubsystem } from '~/app/shared/models/nvmeof';
3+
4+
@Component({
5+
selector: 'cd-nvmeof-subsystems-details',
6+
templateUrl: './nvmeof-subsystems-details.component.html',
7+
styleUrls: ['./nvmeof-subsystems-details.component.scss']
8+
})
9+
export class NvmeofSubsystemsDetailsComponent implements OnChanges {
10+
@Input()
11+
selection: NvmeofSubsystem;
12+
13+
selectedItem: any;
14+
data: any;
15+
16+
ngOnChanges() {
17+
if (this.selection) {
18+
this.selectedItem = this.selection;
19+
this.data = {};
20+
this.data[$localize`Serial Number`] = this.selectedItem.serial_number;
21+
this.data[$localize`Model Number`] = this.selectedItem.model_number;
22+
this.data[$localize`Minimum Controller Identifier`] = this.selectedItem.min_cntlid;
23+
this.data[$localize`Maximum Controller Identifier`] = this.selectedItem.max_cntlid;
24+
this.data[$localize`Subsystem Type`] = this.selectedItem.subtype;
25+
}
26+
}
27+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<cd-modal [pageURL]="pageURL"
2+
[modalRef]="activeModal">
3+
<span class="modal-title"
4+
i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
5+
<ng-container class="modal-content">
6+
<form name="subsystemForm"
7+
#formDir="ngForm"
8+
[formGroup]="subsystemForm"
9+
novalidate>
10+
<div class="modal-body">
11+
<!-- NQN -->
12+
<div class="form-group row">
13+
<label class="cd-col-form-label"
14+
for="nqn">
15+
<span class="required"
16+
i18n>NQN</span>
17+
</label>
18+
<div class="cd-col-form-input">
19+
<input name="nqn"
20+
class="form-control"
21+
type="text"
22+
formControlName="nqn">
23+
<cd-help-text>
24+
The NVMe Qualified Name (NQN) is a unique and permanent name for the lifetime of the subsystem.
25+
</cd-help-text>
26+
<span class="invalid-feedback"
27+
*ngIf="subsystemForm.showError('nqn', formDir, 'required')"
28+
i18n>This field is required.</span>
29+
<span class="invalid-feedback"
30+
*ngIf="subsystemForm.showError('nqn', formDir, 'unique')"
31+
i18n>This NQN is already in use.</span>
32+
<span class="invalid-feedback"
33+
*ngIf="subsystemForm.showError('nqn', formDir, 'pattern')"
34+
i18n>An NQN should follow the format of<br/>&lt;<code>nqn.$year-$month.$reverseDomainName:$definedName</code>".&gt;</span>
35+
<span class="invalid-feedback"
36+
*ngIf="subsystemForm.showError('nqn', formDir, 'maxLength')"
37+
i18n>An NQN should not be more than 223 bytes in length.</span>
38+
</div>
39+
</div>
40+
<!-- Maximum Namespaces -->
41+
<div class="form-group row">
42+
<label class="cd-col-form-label"
43+
for="max_namespaces">
44+
<span i18n>Maximum Namespaces</span>
45+
</label>
46+
<div class="cd-col-form-input">
47+
<input id="max_namespaces"
48+
class="form-control"
49+
type="text"
50+
name="max_namespaces"
51+
formControlName="max_namespaces">
52+
<cd-help-text i18n>The maximum namespaces per subsystem. Default is 256.</cd-help-text>
53+
<span class="invalid-feedback"
54+
*ngIf="subsystemForm.showError('max_namespaces', formDir, 'min')"
55+
i18n>The value must be at least 1.</span>
56+
<span class="invalid-feedback"
57+
*ngIf="subsystemForm.showError('max_namespaces', formDir, 'max')"
58+
i18n>The value cannot be greated than 256.</span>
59+
<span class="invalid-feedback"
60+
*ngIf="subsystemForm.showError('max_namespaces', formDir, 'pattern')"
61+
i18n>The value must be a positive integer.</span>
62+
</div>
63+
</div>
64+
</div>
65+
<div class="modal-footer">
66+
<div class="text-right">
67+
<cd-form-button-panel (submitActionEvent)="onSubmit()"
68+
[form]="subsystemForm"
69+
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
70+
</div>
71+
</div>
72+
</form>
73+
</ng-container>
74+
</cd-modal>

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-subsystems-form/nvmeof-subsystems-form.component.scss

Whitespace-only changes.
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { HttpClientTestingModule } from '@angular/common/http/testing';
2+
import { ReactiveFormsModule } from '@angular/forms';
3+
import { RouterTestingModule } from '@angular/router/testing';
4+
import { ComponentFixture, TestBed } from '@angular/core/testing';
5+
6+
import { ToastrModule } from 'ngx-toastr';
7+
8+
import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
9+
10+
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
11+
import { SharedModule } from '~/app/shared/shared.module';
12+
import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form.component';
13+
import { FormHelper } from '~/testing/unit-test-helper';
14+
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
15+
16+
describe('NvmeofSubsystemsFormComponent', () => {
17+
let component: NvmeofSubsystemsFormComponent;
18+
let fixture: ComponentFixture<NvmeofSubsystemsFormComponent>;
19+
let nvmeofService: NvmeofService;
20+
let form: CdFormGroup;
21+
let formHelper: FormHelper;
22+
const mockTimestamp = 1720693470789;
23+
24+
beforeEach(async () => {
25+
await TestBed.configureTestingModule({
26+
declarations: [NvmeofSubsystemsFormComponent],
27+
providers: [NgbActiveModal],
28+
imports: [
29+
HttpClientTestingModule,
30+
NgbTypeaheadModule,
31+
ReactiveFormsModule,
32+
RouterTestingModule,
33+
SharedModule,
34+
ToastrModule.forRoot()
35+
]
36+
}).compileComponents();
37+
38+
fixture = TestBed.createComponent(NvmeofSubsystemsFormComponent);
39+
component = fixture.componentInstance;
40+
component.ngOnInit();
41+
form = component.subsystemForm;
42+
formHelper = new FormHelper(form);
43+
spyOn(Date, 'now').and.returnValue(mockTimestamp);
44+
fixture.detectChanges();
45+
});
46+
47+
it('should create', () => {
48+
expect(component).toBeTruthy();
49+
});
50+
51+
describe('should test form', () => {
52+
beforeEach(() => {
53+
nvmeofService = TestBed.inject(NvmeofService);
54+
spyOn(nvmeofService, 'createSubsystem').and.stub();
55+
});
56+
57+
it('should be creating request correctly', () => {
58+
const expectedNqn = 'nqn.2001-07.com.ceph:' + mockTimestamp;
59+
component.onSubmit();
60+
expect(nvmeofService.createSubsystem).toHaveBeenCalledWith({
61+
nqn: expectedNqn,
62+
max_namespaces: 256,
63+
enable_ha: true
64+
});
65+
});
66+
67+
it('should give error on invalid nqn', () => {
68+
formHelper.setValue('nqn', 'nqn:2001-07.com.ceph:');
69+
component.onSubmit();
70+
formHelper.expectError('nqn', 'pattern');
71+
});
72+
73+
it('should give error on invalid max_namespaces', () => {
74+
formHelper.setValue('max_namespaces', -56);
75+
component.onSubmit();
76+
formHelper.expectError('max_namespaces', 'pattern');
77+
});
78+
79+
it('should give error on max_namespaces greater than 256', () => {
80+
formHelper.setValue('max_namespaces', 300);
81+
component.onSubmit();
82+
formHelper.expectError('max_namespaces', 'max');
83+
});
84+
85+
it('should give error on max_namespaces lesser than 1', () => {
86+
formHelper.setValue('max_namespaces', 0);
87+
component.onSubmit();
88+
formHelper.expectError('max_namespaces', 'min');
89+
});
90+
});
91+
});

0 commit comments

Comments
 (0)