Skip to content

Commit 74a58e0

Browse files
authored
Merge pull request ceph#57864 from afreen23/wip-nvmeof-gateways
mgr/dashboard: Introduce NVMe/TCP navigation Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: Nizamudeen A <[email protected]>
2 parents 6e63060 + 27a8b2f commit 74a58e0

File tree

18 files changed

+290
-15
lines changed

18 files changed

+290
-15
lines changed

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,27 @@
11
# -*- coding: utf-8 -*-
2-
from typing import Optional
2+
import logging
3+
from typing import Any, Dict, Optional
34

5+
from .. import mgr
46
from ..model import nvmeof as model
57
from ..security import Scope
8+
from ..services.orchestrator import OrchClient
69
from ..tools import str_to_bool
7-
from . import APIDoc, APIRouter, Endpoint, EndpointDoc, Param, ReadPermission, RESTController
10+
from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, Param, \
11+
ReadPermission, RESTController, UIRouter
12+
13+
logger = logging.getLogger(__name__)
14+
15+
NVME_SCHEMA = {
16+
"available": (bool, "Is NVMe/TCP available?"),
17+
"message": (str, "Descriptions")
18+
}
819

920
try:
1021
from ..services.nvmeof_client import NVMeoFClient, empty_response, \
1122
handle_nvmeof_error, map_collection, map_model
12-
except ImportError:
13-
pass
23+
except ImportError as e:
24+
logger.error("Failed to import NVMeoFClient and related components: %s", e)
1425
else:
1526
@APIRouter("/nvmeof/gateway", Scope.NVME_OF)
1627
@APIDoc("NVMe-oF Gateway Management API", "NVMe-oF Gateway")
@@ -380,3 +391,24 @@ def list(self, nqn: str):
380391
return NVMeoFClient().stub.list_connections(
381392
NVMeoFClient.pb2.list_connections_req(subsystem=nqn)
382393
)
394+
395+
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

src/pybind/mgr/dashboard/frontend/src/app/app-routing.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,11 @@ const routes: Routes = [
179179
component: ServiceFormComponent,
180180
outlet: 'modal'
181181
},
182+
{
183+
path: `${URLVerbs.CREATE}/:type`,
184+
component: ServiceFormComponent,
185+
outlet: 'modal'
186+
},
182187
{
183188
path: `${URLVerbs.EDIT}/:type/:name`,
184189
component: ServiceFormComponent,

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

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { RbdTrashListComponent } from './rbd-trash-list/rbd-trash-list.component
3838
import { RbdTrashMoveModalComponent } from './rbd-trash-move-modal/rbd-trash-move-modal.component';
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';
41+
import { NvmeofGatewayComponent } from './nvmeof-gateway/nvmeof-gateway.component';
4142

4243
@NgModule({
4344
imports: [
@@ -77,7 +78,8 @@ import { RbdTrashRestoreModalComponent } from './rbd-trash-restore-modal/rbd-tra
7778
RbdConfigurationListComponent,
7879
RbdConfigurationFormComponent,
7980
RbdTabsComponent,
80-
RbdPerformanceComponent
81+
RbdPerformanceComponent,
82+
NvmeofGatewayComponent
8183
],
8284
exports: [RbdConfigurationListComponent, RbdConfigurationFormComponent]
8385
})
@@ -198,6 +200,29 @@ const routes: Routes = [
198200
]
199201
}
200202
]
203+
},
204+
// NVMe/TCP
205+
{
206+
path: 'nvmeof',
207+
canActivate: [ModuleStatusGuardService],
208+
data: {
209+
breadcrumbs: true,
210+
text: 'NVMe/TCP',
211+
path: 'nvmeof',
212+
disableSplit: true,
213+
moduleStatusGuardConfig: {
214+
uiApiPath: 'nvmeof',
215+
redirectTo: 'error',
216+
header: $localize`NVMe/TCP Gateway not configured`,
217+
button_name: $localize`Configure NVMe/TCP`,
218+
button_route: ['/services', { outlets: { modal: ['create', 'nvmeof'] } }],
219+
uiConfig: false
220+
}
221+
},
222+
children: [
223+
{ path: '', redirectTo: 'gateways', pathMatch: 'full' },
224+
{ path: 'gateways', component: NvmeofGatewayComponent, data: { breadcrumbs: 'Gateways' } }
225+
]
201226
}
202227
];
203228

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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>
10+
11+
<legend i18n>
12+
Gateways
13+
<cd-help-text>
14+
The NVMe-oF gateway integrates Ceph with the NVMe over TCP (NVMe/TCP) protocol to provide an NVMe/TCP target that exports RADOS Block Device (RBD) images.
15+
</cd-help-text>
16+
</legend>
17+
<div>
18+
<cd-table [data]="gateways"
19+
(fetchData)="getGateways()"
20+
[columns]="gatewayColumns">
21+
</cd-table>
22+
</div>

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

Whitespace-only changes.
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
2+
import { of } from 'rxjs';
3+
import { NvmeofGatewayComponent } from './nvmeof-gateway.component';
4+
import { NvmeofService } from '../../../shared/api/nvmeof.service';
5+
import { HttpClientModule } from '@angular/common/http';
6+
import { SharedModule } from '~/app/shared/shared.module';
7+
8+
const mockGateways = [
9+
{
10+
cli_version: '',
11+
version: '1.2.5',
12+
name: 'client.nvmeof.rbd.ceph-node-01.jnmnwa',
13+
group: '',
14+
addr: '192.168.100.101',
15+
port: '5500',
16+
load_balancing_group: 1,
17+
spdk_version: '24.01'
18+
}
19+
];
20+
21+
class MockNvmeOfService {
22+
listGateways() {
23+
return of(mockGateways);
24+
}
25+
}
26+
27+
describe('NvmeofGatewayComponent', () => {
28+
let component: NvmeofGatewayComponent;
29+
let fixture: ComponentFixture<NvmeofGatewayComponent>;
30+
31+
beforeEach(fakeAsync(() => {
32+
TestBed.configureTestingModule({
33+
declarations: [NvmeofGatewayComponent],
34+
imports: [HttpClientModule, SharedModule],
35+
providers: [{ provide: NvmeofService, useClass: MockNvmeOfService }]
36+
}).compileComponents();
37+
}));
38+
39+
beforeEach(() => {
40+
fixture = TestBed.createComponent(NvmeofGatewayComponent);
41+
component = fixture.componentInstance;
42+
});
43+
44+
it('should create', () => {
45+
expect(component).toBeTruthy();
46+
});
47+
48+
it('should retrieve gateways', fakeAsync(() => {
49+
component.getGateways();
50+
tick();
51+
expect(component.gateways).toEqual(mockGateways);
52+
}));
53+
});
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { Component, OnInit } from '@angular/core';
2+
3+
import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
4+
import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
5+
import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
6+
import { NvmeofGateway } from '~/app/shared/models/nvmeof';
7+
8+
import { NvmeofService } from '../../../shared/api/nvmeof.service';
9+
10+
@Component({
11+
selector: 'cd-nvmeof-gateway',
12+
templateUrl: './nvmeof-gateway.component.html',
13+
styleUrls: ['./nvmeof-gateway.component.scss']
14+
})
15+
export class NvmeofGatewayComponent extends ListWithDetails implements OnInit {
16+
gateways: NvmeofGateway[] = [];
17+
gatewayColumns: any;
18+
selection = new CdTableSelection();
19+
20+
constructor(private nvmeofService: NvmeofService, public actionLabels: ActionLabelsI18n) {
21+
super();
22+
}
23+
24+
ngOnInit() {
25+
this.gatewayColumns = [
26+
{
27+
name: $localize`Name`,
28+
prop: 'name'
29+
},
30+
{
31+
name: $localize`Address`,
32+
prop: 'addr'
33+
},
34+
{
35+
name: $localize`Port`,
36+
prop: 'port'
37+
}
38+
];
39+
}
40+
41+
getGateways() {
42+
this.nvmeofService.listGateways().subscribe((gateways: NvmeofGateway[] | NvmeofGateway) => {
43+
if (Array.isArray(gateways)) this.gateways = gateways;
44+
else this.gateways = [gateways];
45+
});
46+
}
47+
}

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,7 +222,8 @@
222222
<div class="cd-col-form-input">
223223
<select id="placement"
224224
class="form-select"
225-
formControlName="placement">
225+
formControlName="placement"
226+
(change)="onPlacementChange($event.target.value)">
226227
<option i18n
227228
value="hosts">Hosts</option>
228229
<option i18n

src/pybind/mgr/dashboard/frontend/src/app/ceph/cluster/services/service-form/service-form.component.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -424,10 +424,15 @@ export class ServiceFormComponent extends CdForm implements OnInit {
424424
});
425425
}
426426

427-
ngOnInit(): void {
428-
this.action = this.actionLabels.CREATE;
427+
resolveRoute() {
429428
if (this.router.url.includes('services/(modal:create')) {
430429
this.pageURL = 'services';
430+
this.route.params.subscribe((params: { type: string }) => {
431+
if (params?.type) {
432+
this.serviceType = params.type;
433+
this.serviceForm.get('service_type').setValue(this.serviceType);
434+
}
435+
});
431436
} else if (this.router.url.includes('services/(modal:edit')) {
432437
this.editing = true;
433438
this.pageURL = 'services';
@@ -436,6 +441,11 @@ export class ServiceFormComponent extends CdForm implements OnInit {
436441
this.serviceType = params.type;
437442
});
438443
}
444+
}
445+
446+
ngOnInit(): void {
447+
this.action = this.actionLabels.CREATE;
448+
this.resolveRoute();
439449

440450
this.cephServiceService
441451
.list(new HttpParams({ fromObject: { limit: -1, offset: 0 } }))
@@ -471,6 +481,9 @@ export class ServiceFormComponent extends CdForm implements OnInit {
471481
this.poolService.getList().subscribe((resp: Pool[]) => {
472482
this.pools = resp;
473483
this.rbdPools = this.pools.filter(this.rbdService.isRBDPool);
484+
if (!this.editing && this.serviceType) {
485+
this.onServiceTypeChange(this.serviceType);
486+
}
474487
});
475488

476489
if (this.editing) {
@@ -670,6 +683,8 @@ export class ServiceFormComponent extends CdForm implements OnInit {
670683
case 'smb':
671684
this.serviceForm.get('count').setValue(1);
672685
break;
686+
default:
687+
this.serviceForm.get('count').setValue(null);
673688
}
674689
}
675690

@@ -759,7 +774,7 @@ export class ServiceFormComponent extends CdForm implements OnInit {
759774
}
760775

761776
setNvmeofServiceId(): void {
762-
const defaultRbdPool: string = this.rbdPools.find((p: Pool) => p.pool_name === 'rbd')
777+
const defaultRbdPool: string = this.rbdPools?.find((p: Pool) => p.pool_name === 'rbd')
763778
?.pool_name;
764779
if (defaultRbdPool) {
765780
this.serviceForm.get('pool').setValue(defaultRbdPool);
@@ -796,6 +811,12 @@ export class ServiceFormComponent extends CdForm implements OnInit {
796811
}
797812
}
798813

814+
onPlacementChange(selected: string) {
815+
if (selected === 'label') {
816+
this.serviceForm.get('count').setValue(null);
817+
}
818+
}
819+
799820
onBlockPoolChange() {
800821
const selectedBlockPool = this.serviceForm.get('pool').value;
801822
if (selectedBlockPool) {

src/pybind/mgr/dashboard/frontend/src/app/core/navigation/breadcrumbs/breadcrumbs.component.spec.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ describe('BreadcrumbsComponent', () => {
7979
tick();
8080
expect(component.crumbs).toEqual([
8181
{ path: null, text: 'Cluster' },
82-
{ path: '/hosts', text: 'Hosts' }
82+
{ path: '/hosts', text: 'Hosts', disableSplit: false }
8383
]);
8484
}));
8585

@@ -125,9 +125,9 @@ describe('BreadcrumbsComponent', () => {
125125
});
126126
tick();
127127
expect(component.crumbs).toEqual([
128-
{ path: null, text: 'Block' },
129-
{ path: '/block/rbd', text: 'Images' },
130-
{ path: '/block/rbd/add', text: 'Add' }
128+
{ path: null, text: 'Block', disableSplit: false },
129+
{ path: '/block/rbd', text: 'Images', disableSplit: false },
130+
{ path: '/block/rbd/add', text: 'Add', disableSplit: false }
131131
]);
132132
}));
133133

0 commit comments

Comments
 (0)