import json
import os
from collections import defaultdict
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
import cephfs
import cherrypy
from ..services.exception import handle_cephfs_error
from ..tools import ViewCache, str_to_bool
from . import APIDoc, APIRouter, DeletePermission, Endpoint, EndpointDoc, \
- RESTController, UIRouter, UpdatePermission, allow_empty_body
+ ReadPermission, RESTController, UIRouter, UpdatePermission, \
+ allow_empty_body
GET_QUOTAS_SCHEMA = {
'max_bytes': (int, ''),
self.cephfs_clients = {}
def list(self):
- fsmap = mgr.get("fs_map")
- return fsmap['filesystems']
-
- def create(self, name: str, service_spec: Dict[str, Any]):
+ return CephFS_.list_filesystems(all_info=True)
+
+ def create(
+ self,
+ name: str,
+ service_spec: Dict[str, Any],
+ data_pool: Optional[str] = None,
+ metadata_pool: Optional[str] = None
+ ):
service_spec_str = '1 '
if 'labels' in service_spec['placement']:
for label in service_spec['placement']['labels']:
service_spec_str += f'{host} '
service_spec_str = service_spec_str[:-1]
- error_code, _, err = mgr.remote('volumes', '_cmd_fs_volume_create', None,
- {'name': name, 'placement': service_spec_str})
+ error_code, _, err = mgr.remote(
+ 'volumes',
+ '_cmd_fs_volume_create',
+ None,
+ {
+ 'name': name,
+ 'placement': service_spec_str,
+ 'data_pool': data_pool,
+ 'meta_pool': metadata_pool
+ }
+ )
if error_code != 0:
raise RuntimeError(
f'Error creating volume {name} with placement {str(service_spec)}: {err}')
paths = []
return paths
+ @Endpoint('GET', path='/used-pools')
+ @ReadPermission
+ def ls_used_pools(self):
+ """
+ This API is created just to list all the used pools to the UI
+ so that it can be used for different validation purposes within
+ the UI
+ """
+ pools = []
+ for fs in CephFS_.list_filesystems(all_info=True):
+ pools.extend(fs['mdsmap']['data_pools'] + [fs['mdsmap']['metadata_pool']])
+ return pools
+
@APIRouter('/cephfs/subvolume', Scope.CEPHFS)
@APIDoc('CephFS Subvolume Management API', 'CephFSSubvolume')
</ng-template>
</div>
+ <div class="form-item"
+ *ngIf="!editing">
+ <cds-checkbox id="customPools"
+ name="customPools"
+ formControlName="customPools"
+ i18n>Use existing pools
+ <cd-help-text>Allows you to use replicated pools with 'cephfs' application tag that are already created.</cd-help-text>
+ </cds-checkbox>
+
+ <cd-alert-panel *ngIf="pools.length < 2"
+ type="info"
+ spacingClass="mt-1"
+ i18n>
+ You need to have atleast 2 pools that are empty, applied with cephfs label and not erasure-coded.
+ </cd-alert-panel>
+ </div>
+
+ <!-- Data pool -->
+ <div class="form-item"
+ *ngIf="form.get('customPools')?.value || editing">
+ <cds-text-label for="dataPool"
+ i18n
+ *ngIf="editing">Data pool
+ <input cdsText
+ type="text"
+ placeholder="Pool name..."
+ id="dataPool"
+ name="dataPool"
+ formControlName="dataPool">
+ </cds-text-label>
+ <cds-select label="Data pool"
+ for="dataPool"
+ name="dataPool"
+ id="dataPool"
+ formControlName="dataPool"
+ (valueChange)="onPoolChange($event)"
+ cdRequiredField="Data pool"
+ [invalid]="!form.controls.dataPool.valid && form.controls.dataPool.dirty"
+ [invalidText]="dataPoolError"
+ *ngIf="!editing">
+ <option *ngIf="dataPools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="dataPools !== null && dataPools?.length === 0"
+ [ngValue]="null"
+ i18n>-- No cephfs pools available --</option>
+ <option *ngIf="dataPools !== null && dataPools?.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a pool --</option>
+ <option *ngFor="let pool of dataPools"
+ [value]="pool?.pool_name">{{ pool?.pool_name }}</option>
+ </cds-select>
+ <ng-template #dataPoolError>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('dataPool', formDir, 'required')"
+ i18n>This field is required!</span>
+ </ng-template>
+ </div>
+
+ <!-- Metadata pool -->
+ <div class="form-item"
+ *ngIf="form.get('customPools')?.value || editing">
+ <cds-text-label for="metadataPool"
+ i18n
+ *ngIf="editing">Metadata pool
+ <input cdsText
+ type="text"
+ placeholder="Pool name..."
+ id="metadataPool"
+ name="metadataPool"
+ formControlName="metadataPool">
+ </cds-text-label>
+ <cds-select label="Metadata pool"
+ for="metadataPool"
+ name="metadataPool"
+ id="metadataPool"
+ formControlName="metadataPool"
+ cdRequiredField="Metadata pool"
+ [invalid]="!form.controls.metadataPool.valid && form.controls.metadataPool.dirty"
+ [invalidText]="metadataPoolError"
+ (valueChange)="onPoolChange($event, true)"
+ *ngIf="!editing">
+ <option *ngIf="metadatPools === null"
+ [ngValue]="null"
+ i18n>Loading...</option>
+ <option *ngIf="metadatPools !== null && metadatPools?.length === 0"
+ [ngValue]="null"
+ i18n>-- No cephfs pools available --</option>
+ <option *ngIf="metadatPools !== null && metadatPools?.length > 0"
+ [ngValue]="null"
+ i18n>-- Select a pool --</option>
+ <option *ngFor="let pool of metadatPools"
+ [value]="pool?.pool_name">{{ pool?.pool_name }}</option>
+ </cds-select>
+ <ng-template #metadataPoolError>
+ <span class="invalid-feedback"
+ *ngIf="form.showError('metadataPool', formDir, 'required')"
+ i18n>This field is required!</span>
+ </ng-template>
+ </div>
+
<ng-container *ngIf="orchStatus.available">
<!-- Placement -->
<div class="form-item"
import { By } from '@angular/platform-browser';
import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
import { of } from 'rxjs';
-import { ComboBoxModule, GridModule, InputModule, SelectModule } from 'carbon-components-angular';
+import {
+ CheckboxModule,
+ ComboBoxModule,
+ GridModule,
+ InputModule,
+ SelectModule
+} from 'carbon-components-angular';
describe('CephfsVolumeFormComponent', () => {
let component: CephfsVolumeFormComponent;
GridModule,
InputModule,
SelectModule,
- ComboBoxModule
+ ComboBoxModule,
+ CheckboxModule
],
declarations: [CephfsVolumeFormComponent]
});
import { FinishedTask } from '~/app/shared/models/finished-task';
import { Permission } from '~/app/shared/models/permissions';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { PoolService } from '~/app/shared/api/pool.service';
+import { Pool } from '../../pool/pool';
@Component({
selector: 'cd-cephfs-form',
fsId: number;
disableRename: boolean = true;
hostsAndLabels$: Observable<{ hosts: any[]; labels: any[] }>;
+ pools: Pool[] = [];
+ dataPools: Pool[] = [];
+ metadatPools: Pool[] = [];
fsFailCmd: string;
fsSetCmd: string;
public actionLabels: ActionLabelsI18n,
private hostService: HostService,
private cephfsService: CephfsService,
- private route: ActivatedRoute
+ private route: ActivatedRoute,
+ private poolService: PoolService
) {
super();
this.editing = this.router.url.startsWith(`/cephfs/fs/${URLVerbs.EDIT}`);
})
]
],
- unmanaged: [false]
+ unmanaged: [false],
+ customPools: [false],
+ dataPool: [
+ null,
+ CdValidators.requiredIf({
+ customPools: true
+ })
+ ],
+ metadataPool: [
+ null,
+ CdValidators.requiredIf({
+ customPools: true
+ })
+ ]
});
this.orchService.status().subscribe((status) => {
this.hasOrchestrator = status.available;
this.cephfsService.getCephfs(this.fsId).subscribe((resp: object) => {
this.currentVolumeName = resp['cephfs']['name'];
this.form.get('name').setValue(this.currentVolumeName);
+ const dataPool =
+ resp['cephfs'].pools.find((pool: Pool) => pool.type === 'data')?.pool || '';
+ const metaPool =
+ resp['cephfs'].pools.find((pool: Pool) => pool.type === 'metadata')?.pool || '';
+ this.form.get('dataPool').setValue(dataPool);
+ this.form.get('metadataPool').setValue(metaPool);
+
+ this.form.get('dataPool').disable();
+ this.form.get('metadataPool').disable();
this.disableRename = !(
!resp['cephfs']['flags']['joinable'] && resp['cephfs']['flags']['refuse_client_session']
}
});
} else {
+ forkJoin({
+ usedPools: this.cephfsService.getUsedPools(),
+ pools: this.poolService.getList()
+ }).subscribe(({ usedPools, pools }) => {
+ // filtering pools if
+ // * pool is labelled with cephfs
+ // * its not already used by cephfs
+ // * its not erasure coded
+ // * and only if its empty
+ const filteredPools = Object.values(pools).filter(
+ (pool: Pool) =>
+ this.cephfsService.isCephFsPool(pool) &&
+ !usedPools.includes(pool.pool) &&
+ pool.type !== 'erasure' &&
+ pool.stats.bytes_used.latest === 0
+ );
+ if (filteredPools.length < 2) this.form.get('customPools').disable();
+ this.pools = filteredPools;
+ this.metadatPools = this.dataPools = this.pools;
+ });
+
this.hostsAndLabels$ = forkJoin({
hosts: this.hostService.getAllHosts(),
labels: this.hostService.getLabels()
this.loadingReady();
}
+ onPoolChange(poolName: string, metadataChange = false) {
+ if (!metadataChange) {
+ this.metadatPools = this.pools.filter((pool: Pool) => pool.pool_name != poolName);
+ } else this.dataPools = this.pools.filter((pool: Pool) => pool.pool_name !== poolName);
+ }
+
multiSelector(event: any, field: 'label' | 'hosts') {
if (field === 'label') this.selectedLabels = event.map((label: any) => label.content);
else this.selectedHosts = event.map((host: any) => host.content);
break;
}
+ const dataPool = values['dataPool'];
+ const metadataPool = values['metadataPool'];
+
const self = this;
let taskUrl = `${BASE_URL}/${URLVerbs.CREATE}`;
this.taskWrapperService
task: new FinishedTask(taskUrl, {
volumeName: volumeName
}),
- call: this.cephfsService.create(this.form.get('name').value, serviceSpec)
+ call: this.cephfsService.create(
+ this.form.get('name').value,
+ serviceSpec,
+ dataPool,
+ metadataPool
+ )
})
.subscribe({
error() {
</cd-table-actions>
</div>
</cd-table>
+
+<ng-template #deleteTpl>
+ <cd-alert-panel type="danger"
+ i18n>
+ This will remove its data and metadata pools. It'll also remove the MDS daemon associated with the volume.
+ </cd-alert-panel>
+</ng-template>
-import { Component, OnInit } from '@angular/core';
+import { Component, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Permissions } from '~/app/shared/models/permissions';
import { Router } from '@angular/router';
providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
})
export class CephfsListComponent extends ListWithDetails implements OnInit {
+ @ViewChild('deleteTpl', { static: true })
+ deleteTpl: TemplateRef<any>;
+
columns: CdTableColumn[];
filesystems: any = [];
selection = new CdTableSelection();
itemDescription: 'File System',
itemNames: [volName],
actionDescription: 'remove',
+ bodyTemplate: this.deleteTpl,
submitActionObservable: () =>
this.taskWrapper.wrapTaskAroundCall({
task: new FinishedTask('cephfs/remove', { volumeName: volName }),
});
}
- create(name: string, serviceSpec: object) {
+ create(name: string, serviceSpec: object, dataPool = '', metadataPool = '') {
return this.http.post(
this.baseURL,
- { name: name, service_spec: serviceSpec },
+ {
+ name: name,
+ service_spec: serviceSpec,
+ data_pool: dataPool,
+ metadata_pool: metadataPool
+ },
{
observe: 'response'
}
root_squash: rootSquash
});
}
+
+ getUsedPools(): Observable<number[]> {
+ return this.http.get<number[]>(`${this.baseUiURL}/used-pools`);
+ }
}
application/json:
schema:
properties:
+ data_pool:
+ type: string
+ metadata_pool:
+ type: string
name:
type: string
service_spec:
class CephFS(object):
@classmethod
- def list_filesystems(cls):
+ def list_filesystems(cls, all_info=False):
fsmap = mgr.get("fs_map")
+
+ if all_info:
+ return fsmap['filesystems']
return [{'id': fs['id'], 'name': fs['mdsmap']['fs_name']}
for fs in fsmap['filesystems']]