Skip to content

Commit 2fd1280

Browse files
authored
Merge pull request ceph#58534 from afreen23/wip-nvmeof-initiators
mgr/dashboard: Add initiators Reviewed-by: Aashish Sharma <[email protected]> Reviewed-by: Anthony D Atri <[email protected]> Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: Ernesto Puerta <[email protected]> Reviewed-by: Nizamudeen A <[email protected]>
2 parents bcd278a + 1f82dc8 commit 2fd1280

22 files changed

+674
-85
lines changed

src/pybind/mgr/dashboard/controllers/nvmeof.py

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@
77
from ..security import Scope
88
from ..services.orchestrator import OrchClient
99
from ..tools import str_to_bool
10-
from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, Param, \
11-
ReadPermission, RESTController, UIRouter
10+
from . import APIDoc, APIRouter, BaseController, CreatePermission, \
11+
DeletePermission, Endpoint, EndpointDoc, Param, ReadPermission, \
12+
RESTController, UIRouter
1213

1314
logger = logging.getLogger(__name__)
1415

@@ -392,23 +393,65 @@ def list(self, nqn: str):
392393
NVMeoFClient.pb2.list_connections_req(subsystem=nqn)
393394
)
394395

396+
@UIRouter('/nvmeof', Scope.NVME_OF)
397+
class NVMeoFTcpUI(BaseController):
398+
@Endpoint('GET', '/status')
399+
@ReadPermission
400+
@EndpointDoc("Display NVMe/TCP service status",
401+
responses={200: NVME_SCHEMA})
402+
def status(self) -> dict:
403+
status: Dict[str, Any] = {'available': True, 'message': None}
404+
orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
405+
if orch_backend == 'cephadm':
406+
orch = OrchClient.instance()
407+
orch_status = orch.status()
408+
if not orch_status['available']:
409+
return status
410+
if not orch.services.list_daemons(daemon_type='nvmeof'):
411+
status["available"] = False
412+
status["message"] = 'An NVMe/TCP service must be created.'
413+
return status
414+
415+
@Endpoint('POST', "/subsystem/{subsystem_nqn}/host")
416+
@EndpointDoc("Add one or more initiator hosts to an NVMeoF subsystem",
417+
parameters={
418+
'subsystem_nqn': (str, 'Subsystem NQN'),
419+
"host_nqn": Param(str, 'Comma separated list of NVMeoF host NQNs'),
420+
})
421+
@empty_response
422+
@handle_nvmeof_error
423+
@CreatePermission
424+
def add(self, subsystem_nqn: str, host_nqn: str = ""):
425+
response = None
426+
all_host_nqns = host_nqn.split(',')
427+
428+
for nqn in all_host_nqns:
429+
response = NVMeoFClient().stub.add_host(
430+
NVMeoFClient.pb2.add_host_req(subsystem_nqn=subsystem_nqn, host_nqn=nqn)
431+
)
432+
if response.status != 0:
433+
return response
434+
return response
395435

396-
@UIRouter('/nvmeof', Scope.NVME_OF)
397-
@APIDoc("NVMe/TCP Management API", "NVMe/TCP")
398-
class NVMeoFStatus(BaseController):
399-
@Endpoint()
400-
@ReadPermission
401-
@EndpointDoc("Display NVMe/TCP service Status",
402-
responses={200: NVME_SCHEMA})
403-
def status(self) -> dict:
404-
status: Dict[str, Any] = {'available': True, 'message': None}
405-
orch_backend = mgr.get_module_option_ex('orchestrator', 'orchestrator')
406-
if orch_backend == 'cephadm':
407-
orch = OrchClient.instance()
408-
orch_status = orch.status()
409-
if not orch_status['available']:
410-
return status
411-
if not orch.services.list_daemons(daemon_type='nvmeof'):
412-
status["available"] = False
413-
status["message"] = 'Create an NVMe/TCP service to get started.'
414-
return status
436+
@Endpoint(method='DELETE', path="/subsystem/{subsystem_nqn}/host/{host_nqn}")
437+
@EndpointDoc("Remove on or more initiator hosts from an NVMeoF subsystem",
438+
parameters={
439+
"subsystem_nqn": Param(str, "NVMeoF subsystem NQN"),
440+
"host_nqn": Param(str, 'Comma separated list of NVMeoF host NQN.'),
441+
})
442+
@empty_response
443+
@handle_nvmeof_error
444+
@DeletePermission
445+
def remove(self, subsystem_nqn: str, host_nqn: str):
446+
response = None
447+
to_delete_nqns = host_nqn.split(',')
448+
449+
for del_nqn in to_delete_nqns:
450+
response = NVMeoFClient().stub.remove_host(
451+
NVMeoFClient.pb2.remove_host_req(subsystem_nqn=subsystem_nqn, host_nqn=del_nqn)
452+
)
453+
if response.status != 0:
454+
return response
455+
logger.info("removed host %s from subsystem %s", del_nqn, subsystem_nqn)
456+
457+
return response

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

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ import { NvmeofListenersFormComponent } from './nvmeof-listeners-form/nvmeof-lis
4747
import { NvmeofListenersListComponent } from './nvmeof-listeners-list/nvmeof-listeners-list.component';
4848
import { NvmeofNamespacesListComponent } from './nvmeof-namespaces-list/nvmeof-namespaces-list.component';
4949
import { NvmeofNamespacesFormComponent } from './nvmeof-namespaces-form/nvmeof-namespaces-form.component';
50+
import { NvmeofInitiatorsListComponent } from './nvmeof-initiators-list/nvmeof-initiators-list.component';
51+
import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form/nvmeof-initiators-form.component';
5052

5153
@NgModule({
5254
imports: [
@@ -95,7 +97,9 @@ import { NvmeofNamespacesFormComponent } from './nvmeof-namespaces-form/nvmeof-n
9597
NvmeofListenersFormComponent,
9698
NvmeofListenersListComponent,
9799
NvmeofNamespacesListComponent,
98-
NvmeofNamespacesFormComponent
100+
NvmeofNamespacesFormComponent,
101+
NvmeofInitiatorsListComponent,
102+
NvmeofInitiatorsFormComponent
99103
],
100104
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
101105
})
@@ -249,15 +253,11 @@ const routes: Routes = [
249253
component: NvmeofSubsystemsFormComponent,
250254
outlet: 'modal'
251255
},
252-
{
253-
path: `${URLVerbs.EDIT}/:subsystem_nqn/:max_ns`,
254-
component: NvmeofSubsystemsFormComponent,
255-
outlet: 'modal'
256-
},
257256
// listeners
258257
{
259258
path: `${URLVerbs.CREATE}/:subsystem_nqn/listener`,
260-
component: NvmeofListenersFormComponent
259+
component: NvmeofListenersFormComponent,
260+
outlet: 'modal'
261261
},
262262
// namespaces
263263
{
@@ -269,6 +269,12 @@ const routes: Routes = [
269269
path: `${URLVerbs.EDIT}/:subsystem_nqn/namespace/:nsid`,
270270
component: NvmeofNamespacesFormComponent,
271271
outlet: 'modal'
272+
},
273+
// initiators
274+
{
275+
path: `${URLVerbs.ADD}/:subsystem_nqn/initiator`,
276+
component: NvmeofInitiatorsFormComponent,
277+
outlet: 'modal'
272278
}
273279
]
274280
},
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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="initiatorForm"
6+
#formDir="ngForm"
7+
[formGroup]="initiatorForm"
8+
novalidate>
9+
<div class="modal-body">
10+
<!-- Hosts -->
11+
<div class="form-group row">
12+
<label class="cd-col-form-label required"
13+
i18n>Hosts
14+
</label>
15+
<div class="cd-col-form-input">
16+
<!-- Add host -->
17+
<div class="custom-control custom-checkbox"
18+
formGroupName="addHost">
19+
<input type="checkbox"
20+
class="custom-control-input"
21+
id="addHostCheck"
22+
name="addHostCheck"
23+
formControlName="addHostCheck"
24+
(change)="setAddHostCheck()"/>
25+
<label class="custom-control-label mb-0"
26+
for="addHostCheck"
27+
i18n>Add host</label>
28+
<cd-help-text>
29+
<span i18n>Allow specific hosts to run NVMe/TCP commands to the NVMe subsystem.</span>
30+
</cd-help-text>
31+
<div formArrayName="addedHosts"
32+
*ngIf="initiatorForm.get('addHost.addHostCheck').value" >
33+
<div *ngFor="let host of addedHosts.controls; let hi = index"
34+
class="input-group cd-mb my-1">
35+
<input class="cd-form-control"
36+
type="text"
37+
i18n-placeholder
38+
placeholder="Add host nqn"
39+
[required]="!initiatorForm.getValue('allowAnyHost')"
40+
[formControlName]="hi"/>
41+
<button class="btn btn-light"
42+
type="button"
43+
id="add-button-{{hi}}"
44+
[disabled]="initiatorForm.get('addHost.addedHosts').controls[hi].invalid
45+
|| initiatorForm.get('addHost.addedHosts').errors?.duplicate
46+
|| initiatorForm.get('addHost.addedHosts').controls.length === 32
47+
|| (initiatorForm.get('addHost.addedHosts').controls.length !== 1 && initiatorForm.get('addHost.addedHosts').controls.length !== hi+1)"
48+
(click)="addHost()">
49+
<i class="fa fa-plus"></i>
50+
</button>
51+
<button class="btn btn-light"
52+
type="button"
53+
id="delete-button-{{hi}}"
54+
[disabled]="addedHosts.controls.length === 1"
55+
(click)="removeHost(hi)">
56+
<i class="fa fa-trash-o"></i>
57+
</button>
58+
<ng-container *ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].invalid
59+
&& (initiatorForm.get('addHost.addedHosts').controls[hi].dirty
60+
|| initiatorForm.get('addHost.addedHosts').controls[hi].touched)">
61+
<span class="invalid-feedback"
62+
*ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].errors?.required"
63+
i18n>This field is required.</span>
64+
<span class="invalid-feedback"
65+
*ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].errors?.pattern"
66+
i18n>Expected NQN format<br/>&lt;<code>nqn.$year-$month.$reverseDomainName:$utf8-string</code>".&gt; or <br/>&lt;<code>nqn.2014-08.org.nvmexpress:uuid:$UUID-string</code>".&gt;</span>
67+
<span class="invalid-feedback"
68+
*ngIf="initiatorForm.get('addHost.addedHosts').controls[hi].errors?.maxLength"
69+
i18n>An NQN may not be more than 223 bytes in length.</span>
70+
</ng-container>
71+
</div>
72+
<span class="invalid-feedback"
73+
*ngIf="initiatorForm.get('addHost.addedHosts').errors?.duplicate"
74+
i18n>Duplicate entry detected. Enter a unique value.</span>
75+
</div>
76+
</div>
77+
<!-- Allow any host -->
78+
<div class="custom-control custom-checkbox pt-0">
79+
<input type="checkbox"
80+
class="custom-control-input"
81+
id="allowAnyHost"
82+
name="allowAnyHost"
83+
formControlName="allowAnyHost"/>
84+
<label class="custom-control-label"
85+
for="allowAnyHost"
86+
i18n>Allow any host</label>
87+
<cd-alert-panel *ngIf="initiatorForm.getValue('allowAnyHost')"
88+
[showTitle]="false"
89+
type="warning">Allowing any host to connect to the NVMe/TCP gateway may pose security risks.
90+
</cd-alert-panel>
91+
</div>
92+
</div>
93+
</div>
94+
</div>
95+
<div class="modal-footer">
96+
<div class="text-right">
97+
<cd-form-button-panel (submitActionEvent)="onSubmit()"
98+
[form]="initiatorForm"
99+
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
100+
</div>
101+
</div>
102+
</form>
103+
</ng-container>
104+
</cd-modal>

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

Whitespace-only changes.
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
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 { SharedModule } from '~/app/shared/shared.module';
11+
import { NvmeofService } from '~/app/shared/api/nvmeof.service';
12+
13+
import { NvmeofInitiatorsFormComponent } from './nvmeof-initiators-form.component';
14+
15+
describe('NvmeofInitiatorsFormComponent', () => {
16+
let component: NvmeofInitiatorsFormComponent;
17+
let fixture: ComponentFixture<NvmeofInitiatorsFormComponent>;
18+
let nvmeofService: NvmeofService;
19+
const mockTimestamp = 1720693470789;
20+
21+
beforeEach(async () => {
22+
spyOn(Date, 'now').and.returnValue(mockTimestamp);
23+
await TestBed.configureTestingModule({
24+
declarations: [NvmeofInitiatorsFormComponent],
25+
providers: [NgbActiveModal],
26+
imports: [
27+
HttpClientTestingModule,
28+
NgbTypeaheadModule,
29+
ReactiveFormsModule,
30+
RouterTestingModule,
31+
SharedModule,
32+
ToastrModule.forRoot()
33+
]
34+
}).compileComponents();
35+
36+
fixture = TestBed.createComponent(NvmeofInitiatorsFormComponent);
37+
component = fixture.componentInstance;
38+
component.ngOnInit();
39+
fixture.detectChanges();
40+
});
41+
42+
it('should create', () => {
43+
expect(component).toBeTruthy();
44+
});
45+
46+
describe('should test form', () => {
47+
beforeEach(() => {
48+
nvmeofService = TestBed.inject(NvmeofService);
49+
spyOn(nvmeofService, 'addInitiators').and.stub();
50+
});
51+
52+
it('should be creating request correctly', () => {
53+
const subsystemNQN = 'nqn.2001-07.com.ceph:' + mockTimestamp;
54+
component.subsystemNQN = subsystemNQN;
55+
component.onSubmit();
56+
expect(nvmeofService.addInitiators).toHaveBeenCalledWith(subsystemNQN, {
57+
host_nqn: ''
58+
});
59+
});
60+
});
61+
});

0 commit comments

Comments
 (0)