Skip to content

Commit 8628b46

Browse files
committed
mgr/dashboard: use existing pools for cephfs vol creation
We can use the newly introduced data and metadata params to create a vol with those pools. UI is being intelligent by filtering out the used pools and only uses the pools that are labeled by cephfs and also not in use. To figure out a pool is in use or not, we are fetching the pool stats and checking its used_bytes. Note: Using ec pools for data pool layout is something discouraged according to offical doc: https://docs.ceph.com/en/latest/cephfs/createfs/#creating-a-file-system We can force it but for now I have disabled it entirely in the dashboard unless people say its okay to do it. One more extra thing I am doing here is to add a note on deleting a filesystem that the underlying pools and mds daemons will be removed. Fixes: https://tracker.ceph.com/issues/70600 Signed-off-by: Nizamudeen A <[email protected]>
1 parent 6fc1a6d commit 8628b46

File tree

9 files changed

+243
-17
lines changed

9 files changed

+243
-17
lines changed

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

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import json
55
import os
66
from collections import defaultdict
7-
from typing import Any, Dict, List
7+
from typing import Any, Dict, List, Optional
88

99
import cephfs
1010
import cherrypy
@@ -17,7 +17,8 @@
1717
from ..services.exception import handle_cephfs_error
1818
from ..tools import ViewCache, str_to_bool
1919
from . import APIDoc, APIRouter, DeletePermission, Endpoint, EndpointDoc, \
20-
RESTController, UIRouter, UpdatePermission, allow_empty_body
20+
ReadPermission, RESTController, UIRouter, UpdatePermission, \
21+
allow_empty_body
2122

2223
GET_QUOTAS_SCHEMA = {
2324
'max_bytes': (int, ''),
@@ -42,10 +43,15 @@ def __init__(self): # pragma: no cover
4243
self.cephfs_clients = {}
4344

4445
def list(self):
45-
fsmap = mgr.get("fs_map")
46-
return fsmap['filesystems']
47-
48-
def create(self, name: str, service_spec: Dict[str, Any]):
46+
return CephFS_.list_filesystems(all_info=True)
47+
48+
def create(
49+
self,
50+
name: str,
51+
service_spec: Dict[str, Any],
52+
data_pool: Optional[str] = None,
53+
metadata_pool: Optional[str] = None
54+
):
4955
service_spec_str = '1 '
5056
if 'labels' in service_spec['placement']:
5157
for label in service_spec['placement']['labels']:
@@ -56,8 +62,17 @@ def create(self, name: str, service_spec: Dict[str, Any]):
5662
service_spec_str += f'{host} '
5763
service_spec_str = service_spec_str[:-1]
5864

59-
error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_create', None,
60-
{'name': name, 'placement': service_spec_str})
65+
error_code, _, err = mgr.remote(
66+
'volumes',
67+
'_cmd_fs_volume_create',
68+
None,
69+
{
70+
'name': name,
71+
'placement': service_spec_str,
72+
'data_pool': data_pool,
73+
'meta_pool': metadata_pool
74+
}
75+
)
6176
if error_code != 0:
6277
raise RuntimeError(
6378
f'Error creating volume {name} with placement {str(service_spec)}: {err}')
@@ -720,6 +735,19 @@ def ls_dir(self, fs_id, path=None, depth=1):
720735
paths = []
721736
return paths
722737

738+
@Endpoint('GET', path='/used-pools')
739+
@ReadPermission
740+
def ls_used_pools(self):
741+
"""
742+
This API is created just to list all the used pools to the UI
743+
so that it can be used for different validation purposes within
744+
the UI
745+
"""
746+
pools = []
747+
for fs in CephFS_.list_filesystems(all_info=True):
748+
pools.extend(fs['mdsmap']['data_pools'] + [fs['mdsmap']['metadata_pool']])
749+
return pools
750+
723751

724752
@APIRouter('/cephfs/subvolume', Scope.CEPHFS)
725753
@APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume')

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.html

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,107 @@
5353
</ng-template>
5454
</div>
5555

56+
<div class="form-item"
57+
*ngIf="!editing">
58+
<cds-checkbox id="customPools"
59+
name="customPools"
60+
formControlName="customPools"
61+
i18n>Use existing pools
62+
<cd-help-text>Allows you to use replicated pools with 'cephfs' application tag that are already created.</cd-help-text>
63+
</cds-checkbox>
64+
65+
<cd-alert-panel *ngIf="pools.length < 2"
66+
type="info"
67+
spacingClass="mt-1"
68+
i18n>
69+
You need to have atleast 2 pools that are empty, applied with cephfs label and not erasure-coded.
70+
</cd-alert-panel>
71+
</div>
72+
73+
<!-- Data pool -->
74+
<div class="form-item"
75+
*ngIf="form.get('customPools')?.value || editing">
76+
<cds-text-label for="dataPool"
77+
i18n
78+
*ngIf="editing">Data pool
79+
<input cdsText
80+
type="text"
81+
placeholder="Pool name..."
82+
id="dataPool"
83+
name="dataPool"
84+
formControlName="dataPool">
85+
</cds-text-label>
86+
<cds-select label="Data pool"
87+
for="dataPool"
88+
name="dataPool"
89+
id="dataPool"
90+
formControlName="dataPool"
91+
(valueChange)="onPoolChange($event)"
92+
cdRequiredField="Data pool"
93+
[invalid]="!form.controls.dataPool.valid && form.controls.dataPool.dirty"
94+
[invalidText]="dataPoolError"
95+
*ngIf="!editing">
96+
<option *ngIf="dataPools === null"
97+
[ngValue]="null"
98+
i18n>Loading...</option>
99+
<option *ngIf="dataPools !== null && dataPools?.length === 0"
100+
[ngValue]="null"
101+
i18n>-- No cephfs pools available --</option>
102+
<option *ngIf="dataPools !== null && dataPools?.length > 0"
103+
[ngValue]="null"
104+
i18n>-- Select a pool --</option>
105+
<option *ngFor="let pool of dataPools"
106+
[value]="pool?.pool_name">{{ pool?.pool_name }}</option>
107+
</cds-select>
108+
<ng-template #dataPoolError>
109+
<span class="invalid-feedback"
110+
*ngIf="form.showError('dataPool', formDir, 'required')"
111+
i18n>This field is required!</span>
112+
</ng-template>
113+
</div>
114+
115+
<!-- Metadata pool -->
116+
<div class="form-item"
117+
*ngIf="form.get('customPools')?.value || editing">
118+
<cds-text-label for="metadataPool"
119+
i18n
120+
*ngIf="editing">Metadata pool
121+
<input cdsText
122+
type="text"
123+
placeholder="Pool name..."
124+
id="metadataPool"
125+
name="metadataPool"
126+
formControlName="metadataPool">
127+
</cds-text-label>
128+
<cds-select label="Metadata pool"
129+
for="metadataPool"
130+
name="metadataPool"
131+
id="metadataPool"
132+
formControlName="metadataPool"
133+
cdRequiredField="Metadata pool"
134+
[invalid]="!form.controls.metadataPool.valid && form.controls.metadataPool.dirty"
135+
[invalidText]="metadataPoolError"
136+
(valueChange)="onPoolChange($event, true)"
137+
*ngIf="!editing">
138+
<option *ngIf="metadatPools === null"
139+
[ngValue]="null"
140+
i18n>Loading...</option>
141+
<option *ngIf="metadatPools !== null && metadatPools?.length === 0"
142+
[ngValue]="null"
143+
i18n>-- No cephfs pools available --</option>
144+
<option *ngIf="metadatPools !== null && metadatPools?.length > 0"
145+
[ngValue]="null"
146+
i18n>-- Select a pool --</option>
147+
<option *ngFor="let pool of metadatPools"
148+
[value]="pool?.pool_name">{{ pool?.pool_name }}</option>
149+
</cds-select>
150+
<ng-template #metadataPoolError>
151+
<span class="invalid-feedback"
152+
*ngIf="form.showError('metadataPool', formDir, 'required')"
153+
i18n>This field is required!</span>
154+
</ng-template>
155+
</div>
156+
56157
<ng-container *ngIf="orchStatus.available">
57158
<!-- Placement -->
58159
<div class="form-item"

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.spec.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,13 @@ import { ReactiveFormsModule } from '@angular/forms';
1010
import { By } from '@angular/platform-browser';
1111
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
1212
import { of } from 'rxjs';
13-
import { ComboBoxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
13+
import {
14+
CheckboxModule,
15+
ComboBoxModule,
16+
GridModule,
17+
InputModule,
18+
SelectModule
19+
} from 'carbon-components-angular';
1420

1521
describe('CephfsVolumeFormComponent', () => {
1622
let component: CephfsVolumeFormComponent;
@@ -29,7 +35,8 @@ describe('CephfsVolumeFormComponent', () => {
2935
GridModule,
3036
InputModule,
3137
SelectModule,
32-
ComboBoxModule
38+
ComboBoxModule,
39+
CheckboxModule
3340
],
3441
declarations: [CephfsVolumeFormComponent]
3542
});

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-form/cephfs-form.component.ts

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import { CdValidators } from '~/app/shared/forms/cd-validators';
1919
import { FinishedTask } from '~/app/shared/models/finished-task';
2020
import { Permission } from '~/app/shared/models/permissions';
2121
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
22+
import { PoolService } from '~/app/shared/api/pool.service';
23+
import { Pool } from '../../pool/pool';
2224

2325
@Component({
2426
selector: 'cd-cephfs-form',
@@ -51,6 +53,9 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
5153
fsId: number;
5254
disableRename: boolean = true;
5355
hostsAndLabels$: Observable<{ hosts: any[]; labels: any[] }>;
56+
pools: Pool[] = [];
57+
dataPools: Pool[] = [];
58+
metadatPools: Pool[] = [];
5459

5560
fsFailCmd: string;
5661
fsSetCmd: string;
@@ -66,7 +71,8 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
6671
public actionLabels: ActionLabelsI18n,
6772
private hostService: HostService,
6873
private cephfsService: CephfsService,
69-
private route: ActivatedRoute
74+
private route: ActivatedRoute,
75+
private poolService: PoolService
7076
) {
7177
super();
7278
this.editing = this.router.url.startsWith(`/cephfs/fs/${URLVerbs.EDIT}`);
@@ -94,7 +100,20 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
94100
})
95101
]
96102
],
97-
unmanaged: [false]
103+
unmanaged: [false],
104+
customPools: [false],
105+
dataPool: [
106+
null,
107+
CdValidators.requiredIf({
108+
customPools: true
109+
})
110+
],
111+
metadataPool: [
112+
null,
113+
CdValidators.requiredIf({
114+
customPools: true
115+
})
116+
]
98117
});
99118
this.orchService.status().subscribe((status) => {
100119
this.hasOrchestrator = status.available;
@@ -111,6 +130,15 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
111130
this.cephfsService.getCephfs(this.fsId).subscribe((resp: object) => {
112131
this.currentVolumeName = resp['cephfs']['name'];
113132
this.form.get('name').setValue(this.currentVolumeName);
133+
const dataPool =
134+
resp['cephfs'].pools.find((pool: Pool) => pool.type === 'data')?.pool || '';
135+
const metaPool =
136+
resp['cephfs'].pools.find((pool: Pool) => pool.type === 'metadata')?.pool || '';
137+
this.form.get('dataPool').setValue(dataPool);
138+
this.form.get('metadataPool').setValue(metaPool);
139+
140+
this.form.get('dataPool').disable();
141+
this.form.get('metadataPool').disable();
114142

115143
this.disableRename = !(
116144
!resp['cephfs']['flags']['joinable'] && resp['cephfs']['flags']['refuse_client_session']
@@ -122,6 +150,27 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
122150
}
123151
});
124152
} else {
153+
forkJoin({
154+
usedPools: this.cephfsService.getUsedPools(),
155+
pools: this.poolService.getList()
156+
}).subscribe(({ usedPools, pools }) => {
157+
// filtering pools if
158+
// * pool is labelled with cephfs
159+
// * its not already used by cephfs
160+
// * its not erasure coded
161+
// * and only if its empty
162+
const filteredPools = Object.values(pools).filter(
163+
(pool: Pool) =>
164+
this.cephfsService.isCephFsPool(pool) &&
165+
!usedPools.includes(pool.pool) &&
166+
pool.type !== 'erasure' &&
167+
pool.stats.bytes_used.latest === 0
168+
);
169+
if (filteredPools.length < 2) this.form.get('customPools').disable();
170+
this.pools = filteredPools;
171+
this.metadatPools = this.dataPools = this.pools;
172+
});
173+
125174
this.hostsAndLabels$ = forkJoin({
126175
hosts: this.hostService.getAllHosts(),
127176
labels: this.hostService.getLabels()
@@ -136,6 +185,12 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
136185
this.loadingReady();
137186
}
138187

188+
onPoolChange(poolName: string, metadataChange = false) {
189+
if (!metadataChange) {
190+
this.metadatPools = this.pools.filter((pool: Pool) => pool.pool_name != poolName);
191+
} else this.dataPools = this.pools.filter((pool: Pool) => pool.pool_name !== poolName);
192+
}
193+
139194
multiSelector(event: any, field: 'label' | 'hosts') {
140195
if (field === 'label') this.selectedLabels = event.map((label: any) => label.content);
141196
else this.selectedHosts = event.map((host: any) => host.content);
@@ -178,14 +233,22 @@ export class CephfsVolumeFormComponent extends CdForm implements OnInit {
178233
break;
179234
}
180235

236+
const dataPool = values['dataPool'];
237+
const metadataPool = values['metadataPool'];
238+
181239
const self = this;
182240
let taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`;
183241
this.taskWrapperService
184242
.wrapTaskAroundCall({
185243
task: new FinishedTask(taskUrl, {
186244
volumeName: volumeName
187245
}),
188-
call: this.cephfsService.create(this.form.get('name').value, serviceSpec)
246+
call: this.cephfsService.create(
247+
this.form.get('name').value,
248+
serviceSpec,
249+
dataPool,
250+
metadataPool
251+
)
189252
})
190253
.subscribe({
191254
error() {

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.html

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,10 @@
2020
</cd-table-actions>
2121
</div>
2222
</cd-table>
23+
24+
<ng-template #deleteTpl>
25+
<cd-alert-panel type="danger"
26+
i18n>
27+
This will remove its data and metadata pools. It'll also remove the MDS daemon associated with the volume.
28+
</cd-alert-panel>
29+
</ng-template>

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-list/cephfs-list.component.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Component, OnInit } from '@angular/core';
1+
import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
22
import { Permissions } from '~/app/shared/models/permissions';
33
import { Router } from '@angular/router';
44

@@ -37,6 +37,9 @@ const BASE_URL = 'cephfs/fs';
3737
providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
3838
})
3939
export class CephfsListComponent extends ListWithDetails implements OnInit {
40+
@ViewChild('deleteTpl', { static: true })
41+
deleteTpl: TemplateRef<any>;
42+
4043
columns: CdTableColumn[];
4144
filesystems: any = [];
4245
selection = new CdTableSelection();
@@ -178,6 +181,7 @@ export class CephfsListComponent extends ListWithDetails implements OnInit {
178181
itemDescription: 'File System',
179182
itemNames: [volName],
180183
actionDescription: 'remove',
184+
bodyTemplate: this.deleteTpl,
181185
submitActionObservable: () =>
182186
this.taskWrapper.wrapTaskAroundCall({
183187
task: new FinishedTask('cephfs/remove', { volumeName: volName }),

0 commit comments

Comments
 (0)