Skip to content

Commit 5b6f1ee

Browse files
authored
Merge pull request ceph#58648 from afreen23/wip-nvmeof-listener
Allow listeners creation and deletion Reviewed-by: Nizamudeen A <[email protected]>
2 parents 6d74208 + a4f2eef commit 5b6f1ee

18 files changed

+554
-13
lines changed

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ import { NvmeofSubsystemsComponent } from './nvmeof-subsystems/nvmeof-subsystems
4343
import { NvmeofSubsystemsDetailsComponent } from './nvmeof-subsystems-details/nvmeof-subsystems-details.component';
4444
import { NvmeofTabsComponent } from './nvmeof-tabs/nvmeof-tabs.component';
4545
import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-subsystems-form.component';
46+
import { NvmeofListenersFormComponent } from './nvmeof-listeners-form/nvmeof-listeners-form.component';
47+
import { NvmeofListenersListComponent } from './nvmeof-listeners-list/nvmeof-listeners-list.component';
4648

4749
@NgModule({
4850
imports: [
@@ -87,7 +89,9 @@ import { NvmeofSubsystemsFormComponent } from './nvmeof-subsystems-form/nvmeof-s
8789
NvmeofSubsystemsComponent,
8890
NvmeofSubsystemsDetailsComponent,
8991
NvmeofTabsComponent,
90-
NvmeofSubsystemsFormComponent
92+
NvmeofSubsystemsFormComponent,
93+
NvmeofListenersFormComponent,
94+
NvmeofListenersListComponent
9195
],
9296
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
9397
})
@@ -244,6 +248,11 @@ const routes: Routes = [
244248
path: `${URLVerbs.EDIT}/:subsystem_nqn`,
245249
component: NvmeofSubsystemsFormComponent,
246250
outlet: 'modal'
251+
},
252+
{
253+
path: `${URLVerbs.CREATE}/:subsystem_nqn/listener`,
254+
component: NvmeofListenersFormComponent,
255+
outlet: 'modal'
247256
}
248257
]
249258
},
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<cd-modal [pageURL]="pageURL">
2+
<span class="modal-title"
3+
i18n>{{ action | titlecase }} {{ resource | upperFirst }}</span>
4+
<ng-container class="modal-content">
5+
<form name="listenerForm"
6+
#formDir="ngForm"
7+
[formGroup]="listenerForm"
8+
novalidate>
9+
<div class="modal-body">
10+
<!-- Host -->
11+
<div class="form-group row">
12+
<label class="cd-col-form-label"
13+
for="host">
14+
<span class="required"
15+
i18n>Host Name</span>
16+
</label>
17+
<div class="cd-col-form-input">
18+
<select id="host"
19+
name="host"
20+
class="form-select"
21+
formControlName="host">
22+
<option *ngIf="hosts === null"
23+
[ngValue]="null"
24+
i18n>Loading...</option>
25+
<option *ngIf="hosts && hosts.length === 0"
26+
[ngValue]="null"
27+
i18n>-- No hosts available --</option>
28+
<option *ngIf="hosts && hosts.length > 0"
29+
[ngValue]="null"
30+
i18n>-- Select a host --</option>
31+
<option *ngFor="let hostsItem of hosts"
32+
[ngValue]="hostsItem">{{ hostsItem.hostname }}</option>
33+
</select>
34+
<cd-help-text i18n>
35+
This hostname uniquely identifies the gateway on which the listener is being set up.
36+
</cd-help-text>
37+
<span class="invalid-feedback"
38+
*ngIf="listenerForm.showError('host', formDir, 'required')"
39+
i18n>This field is required.</span>
40+
</div>
41+
</div>
42+
<!-- Transport Service ID -->
43+
<div class="form-group row">
44+
<label class="cd-col-form-label"
45+
for="trsvcid">
46+
<span i18n>Transport Service ID</span>
47+
</label>
48+
<div class="cd-col-form-input">
49+
<input id="trsvcid"
50+
class="form-control"
51+
type="text"
52+
name="trsvcid"
53+
formControlName="trsvcid">
54+
<cd-help-text i18n>The IP port to use. Default is 4420.</cd-help-text>
55+
<span class="invalid-feedback"
56+
*ngIf="listenerForm.showError('trsvcid', formDir, 'required')"
57+
i18n>This field is required.</span>
58+
<span class="invalid-feedback"
59+
*ngIf="listenerForm.showError('trsvcid', formDir, 'max')"
60+
i18n>The value cannot be greated than 65535.</span>
61+
<span class="invalid-feedback"
62+
*ngIf="listenerForm.showError('trsvcid', formDir, 'pattern')"
63+
i18n>The value must be a positive integer.</span>
64+
</div>
65+
</div>
66+
</div>
67+
<div class="modal-footer">
68+
<div class="text-right">
69+
<cd-form-button-panel (submitActionEvent)="onSubmit()"
70+
[form]="listenerForm"
71+
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
72+
</div>
73+
</div>
74+
</form>
75+
</ng-container>
76+
</cd-modal>

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

Whitespace-only changes.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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+
import { NgbActiveModal, NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
8+
9+
import { SharedModule } from '~/app/shared/shared.module';
10+
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
11+
import { NvmeofListenersFormComponent } from './nvmeof-listeners-form.component';
12+
13+
describe('NvmeofListenersFormComponent', () => {
14+
let component: NvmeofListenersFormComponent;
15+
let fixture: ComponentFixture<NvmeofListenersFormComponent>;
16+
let nvmeofService: NvmeofService;
17+
18+
beforeEach(async () => {
19+
await TestBed.configureTestingModule({
20+
declarations: [NvmeofListenersFormComponent],
21+
providers: [NgbActiveModal],
22+
imports: [
23+
HttpClientTestingModule,
24+
NgbTypeaheadModule,
25+
ReactiveFormsModule,
26+
RouterTestingModule,
27+
SharedModule,
28+
ToastrModule.forRoot()
29+
]
30+
}).compileComponents();
31+
32+
fixture = TestBed.createComponent(NvmeofListenersFormComponent);
33+
component = fixture.componentInstance;
34+
component.ngOnInit();
35+
fixture.detectChanges();
36+
});
37+
38+
it('should create', () => {
39+
expect(component).toBeTruthy();
40+
});
41+
42+
describe('should test form', () => {
43+
beforeEach(() => {
44+
nvmeofService = TestBed.inject(NvmeofService);
45+
spyOn(nvmeofService, 'createListener').and.stub();
46+
});
47+
});
48+
});
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { Component, OnInit } from '@angular/core';
2+
import { UntypedFormControl, Validators } from '@angular/forms';
3+
import { ActivatedRoute, Router } from '@angular/router';
4+
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
5+
import { ListenerRequest, NvmeofService } from '~/app/shared/api/nvmeof.service';
6+
import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
7+
import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
8+
import { FinishedTask } from '~/app/shared/models/finished-task';
9+
import { Permission } from '~/app/shared/models/permissions';
10+
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
11+
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
12+
import { FormatterService } from '~/app/shared/services/formatter.service';
13+
import { CdValidators } from '~/app/shared/forms/cd-validators';
14+
import { DimlessBinaryPipe } from '~/app/shared/pipes/dimless-binary.pipe';
15+
import { HostService } from '~/app/shared/api/host.service';
16+
import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
17+
@Component({
18+
selector: 'cd-nvmeof-listeners-form',
19+
templateUrl: './nvmeof-listeners-form.component.html',
20+
styleUrls: ['./nvmeof-listeners-form.component.scss']
21+
})
22+
export class NvmeofListenersFormComponent implements OnInit {
23+
action: string;
24+
permission: Permission;
25+
hostPermission: Permission;
26+
resource: string;
27+
pageURL: string;
28+
listenerForm: CdFormGroup;
29+
subsystemNQN: string;
30+
hosts: Array<object> = null;
31+
32+
constructor(
33+
public actionLabels: ActionLabelsI18n,
34+
private authStorageService: AuthStorageService,
35+
private taskWrapperService: TaskWrapperService,
36+
private nvmeofService: NvmeofService,
37+
private hostService: HostService,
38+
private router: Router,
39+
private route: ActivatedRoute,
40+
public activeModal: NgbActiveModal,
41+
public formatterService: FormatterService,
42+
public dimlessBinaryPipe: DimlessBinaryPipe
43+
) {
44+
this.permission = this.authStorageService.getPermissions().nvmeof;
45+
this.hostPermission = this.authStorageService.getPermissions().hosts;
46+
this.resource = $localize`Listener`;
47+
this.pageURL = 'block/nvmeof/subsystems';
48+
}
49+
50+
setHosts() {
51+
const hostContext = new CdTableFetchDataContext(() => undefined);
52+
this.hostService.list(hostContext.toParams(), 'false').subscribe((resp: any[]) => {
53+
const nvmeofHosts = resp.filter((r) =>
54+
r.service_instances.some((si: any) => si.type === 'nvmeof')
55+
);
56+
this.hosts = nvmeofHosts.map((h) => ({ hostname: h.hostname, addr: h.addr }));
57+
});
58+
}
59+
60+
ngOnInit() {
61+
this.createForm();
62+
this.action = this.actionLabels.CREATE;
63+
this.route.params.subscribe((params: { subsystem_nqn: string }) => {
64+
this.subsystemNQN = params.subsystem_nqn;
65+
});
66+
this.setHosts();
67+
}
68+
69+
createForm() {
70+
this.listenerForm = new CdFormGroup({
71+
host: new UntypedFormControl(null, {
72+
validators: [Validators.required]
73+
}),
74+
trsvcid: new UntypedFormControl(4420, [
75+
Validators.required,
76+
CdValidators.number(false),
77+
Validators.max(65535)
78+
])
79+
});
80+
}
81+
82+
buildRequest(): ListenerRequest {
83+
const host = this.listenerForm.getValue('host');
84+
let trsvcid = Number(this.listenerForm.getValue('trsvcid'));
85+
if (!trsvcid) trsvcid = 4420;
86+
const request = {
87+
host_name: host.hostname,
88+
traddr: host.addr,
89+
trsvcid
90+
};
91+
return request;
92+
}
93+
94+
onSubmit() {
95+
const component = this;
96+
const taskUrl: string = `nvmeof/listener/${URLVerbs.CREATE}`;
97+
const request = this.buildRequest();
98+
this.taskWrapperService
99+
.wrapTaskAroundCall({
100+
task: new FinishedTask(taskUrl, {
101+
nqn: this.subsystemNQN,
102+
host_name: request.host_name
103+
}),
104+
call: this.nvmeofService.createListener(this.subsystemNQN, request)
105+
})
106+
.subscribe({
107+
error() {
108+
component.listenerForm.setErrors({ cdSubmitButton: true });
109+
},
110+
complete: () => {
111+
this.router.navigate([this.pageURL, { outlets: { modal: null } }]);
112+
}
113+
});
114+
}
115+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<legend>
2+
<cd-help-text>
3+
A listener defines the IP port on the gateway that is to process NVMe/TCP commands and I/O operations.
4+
</cd-help-text>
5+
</legend>
6+
<cd-table [data]="listeners"
7+
columnMode="flex"
8+
(fetchData)="listListeners()"
9+
[columns]="listenerColumns"
10+
identifier="id"
11+
forceIdentifier="true"
12+
selectionType="single"
13+
(updateSelection)="updateSelection($event)">
14+
<div class="table-actions btn-toolbar">
15+
<cd-table-actions [permission]="permission"
16+
[selection]="selection"
17+
class="btn-group"
18+
[tableActions]="tableActions">
19+
</cd-table-actions>
20+
</div>
21+
</cd-table>

src/pybind/mgr/dashboard/frontend/src/app/ceph/block/nvmeof-listeners-list/nvmeof-listeners-list.component.scss

Whitespace-only changes.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
2+
3+
import { NvmeofListenersListComponent } from './nvmeof-listeners-list.component';
4+
import { HttpClientModule } from '@angular/common/http';
5+
import { RouterTestingModule } from '@angular/router/testing';
6+
import { SharedModule } from '~/app/shared/shared.module';
7+
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
8+
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
9+
import { ModalService } from '~/app/shared/services/modal.service';
10+
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
11+
import { of } from 'rxjs';
12+
13+
const mockListeners = [
14+
{
15+
host_name: 'ceph-node-02',
16+
trtype: 'TCP',
17+
traddr: '192.168.100.102',
18+
adrfam: 0,
19+
trsvcid: 4421
20+
}
21+
];
22+
23+
class MockNvmeOfService {
24+
listListeners() {
25+
return of(mockListeners);
26+
}
27+
}
28+
29+
class MockAuthStorageService {
30+
getPermissions() {
31+
return { nvmeof: {} };
32+
}
33+
}
34+
35+
class MockModalService {}
36+
37+
class MockTaskWrapperService {}
38+
39+
describe('NvmeofListenersListComponent', () => {
40+
let component: NvmeofListenersListComponent;
41+
let fixture: ComponentFixture<NvmeofListenersListComponent>;
42+
43+
beforeEach(async () => {
44+
await TestBed.configureTestingModule({
45+
declarations: [NvmeofListenersListComponent],
46+
imports: [HttpClientModule, RouterTestingModule, SharedModule],
47+
providers: [
48+
{ provide: NvmeofService, useClass: MockNvmeOfService },
49+
{ provide: AuthStorageService, useClass: MockAuthStorageService },
50+
{ provide: ModalService, useClass: MockModalService },
51+
{ provide: TaskWrapperService, useClass: MockTaskWrapperService }
52+
]
53+
}).compileComponents();
54+
55+
fixture = TestBed.createComponent(NvmeofListenersListComponent);
56+
component = fixture.componentInstance;
57+
component.ngOnInit();
58+
fixture.detectChanges();
59+
});
60+
61+
it('should create', () => {
62+
expect(component).toBeTruthy();
63+
});
64+
65+
it('should retrieve subsystems', fakeAsync(() => {
66+
component.listListeners();
67+
tick();
68+
expect(component.listeners).toEqual(mockListeners);
69+
}));
70+
});

0 commit comments

Comments
 (0)