From: Dnyaneshwari Talwekar Date: Fri, 9 Jan 2026 09:59:50 +0000 (+0530) Subject: mgr/dashboard: Cephfs mirroring - Entity X-Git-Url: http://git-server-git.apps.pok.os.sepia.ceph.com/?a=commitdiff_plain;h=6485acda206d70aa7236dc33e90b299373da7026;p=ceph.git mgr/dashboard: Cephfs mirroring - Entity Fixes: https://tracker.ceph.com/issues/74366 Signed-off-by: Dnyaneshwari Talwekar --- diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts index 56d3bbb204c..d22fb82c2c3 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-filesystem-selector/cephfs-filesystem-selector.component.ts @@ -1,4 +1,12 @@ -import { Component, OnInit, TemplateRef, ViewChild, inject } from '@angular/core'; +import { + Component, + OnInit, + TemplateRef, + ViewChild, + inject, + Output, + EventEmitter +} from '@angular/core'; import { CephfsService } from '~/app/shared/api/cephfs.service'; import { CdTableColumn } from '~/app/shared/models/cd-table-column'; @@ -32,6 +40,7 @@ export class CephfsFilesystemSelectorComponent implements OnInit { filesystems$: Observable = of([]); selection = new CdTableSelection(); icons = Icons; + @Output() filesystemSelected = new EventEmitter(); mdsStatusLabels: Record = { Active: $localize`Active`, Warning: $localize`Warning`, @@ -111,5 +120,7 @@ export class CephfsFilesystemSelectorComponent implements OnInit { updateSelection(selection: CdTableSelection) { this.selection = selection; + const selectedRow = typeof selection?.first === 'function' ? selection.first() : null; + this.filesystemSelected.emit(selectedRow as FilesystemRow | null); } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.html new file mode 100644 index 00000000000..6dd4aafb144 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.html @@ -0,0 +1,193 @@ +
+
+
Create or select entity
+

+ Choose an existing CephFS mirroring entity or create a new one to be used for mirroring. +

+
+ +
+ + Create new entity + Use existing entity + +
+ + @if (isCreatingNewEntity) { + @if (showCreateRequirementsWarning) { + +
+
Mirroring entity requirements
+
+ This entity will be used by the mirroring service to manage snapshots and replication. + It will require the proper permissions to replicate data. +
+
+
+ } + + @if (showCreateCapabilitiesInfo) { + +
+
Capabilities added automatically.
+
+ If you create a new entity, the following capabilities will be assigned automatically: +
+
    + @for (cap of capabilities; track cap.name) { +
  • + {{ cap.name }}: {{ cap.permission }} +
  • + } +
+
+
+ } + +
+
+ + Ceph entity +
+ + +
+
+ + + @if (entityForm.showError('user_entity', formDir, 'required')) { + This field is required. + } + @if (entityForm.showError('user_entity', formDir, 'forbiddenClientPrefix')) { + Do not include 'client.' prefix. Enter only the entity suffix. + } + +
+
+ + +
+
+ } + + @if (!isCreatingNewEntity && showSelectRequirementsWarning) { + +
+
Mirroring entity requirements
+
+ This selected entity will be used by the mirroring service to manage snapshots and + replication. It must have rwps capabilities on MDS, MON, and OSD. +
+
+
+ @if (showSelectEntityInfo) { + +
+
Entity selection note
+
+ Only entities with valid rwps capabilities can be used for mirroring. +
+
+
+ } + } + + @let entities = (entities$ | async); + @if (!isCreatingNewEntity && entities) { + + + } +
+
+ diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.scss new file mode 100644 index 00000000000..f359d817d0e --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.scss @@ -0,0 +1,17 @@ +@use '@carbon/layout'; + +.scroll-overflow { + max-height: 70vh; + overflow-y: auto; + overflow-x: hidden; +} + +.requirements-list { + list-style-type: disc; + padding-left: var(--cds-spacing-05); + margin-left: layout.$spacing-02; +} + +.list-disc { + list-style-type: disc; +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.spec.ts new file mode 100644 index 00000000000..893cfcf0d65 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.spec.ts @@ -0,0 +1,185 @@ +import { ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; +import { ReactiveFormsModule } from '@angular/forms'; +import { of } from 'rxjs'; + +import { CephfsMirroringEntityComponent } from './cephfs-mirroring-entity.component'; +import { CephfsService } from '~/app/shared/api/cephfs.service'; +import { ClusterService } from '~/app/shared/api/cluster.service'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; + +describe('CephfsMirroringEntityComponent', () => { + let component: CephfsMirroringEntityComponent; + let fixture: ComponentFixture; + + let clusterServiceMock: any; + let cephfsServiceMock: any; + let taskWrapperServiceMock: any; + + beforeEach(async () => { + clusterServiceMock = { + listUser: jest.fn(), + createUser: jest.fn() + }; + + cephfsServiceMock = { + setAuth: jest.fn() + }; + + taskWrapperServiceMock = { + wrapTaskAroundCall: jest.fn() + }; + + await TestBed.configureTestingModule({ + declarations: [CephfsMirroringEntityComponent], + imports: [ReactiveFormsModule], + providers: [ + CdFormBuilder, + { provide: ClusterService, useValue: clusterServiceMock }, + { provide: CephfsService, useValue: cephfsServiceMock }, + { provide: TaskWrapperService, useValue: taskWrapperServiceMock } + ] + }) + .overrideComponent(CephfsMirroringEntityComponent, { + set: { template: '' } + }) + .compileComponents(); + + fixture = TestBed.createComponent(CephfsMirroringEntityComponent); + component = fixture.componentInstance; + + component.selectedFilesystem = { name: 'myfs' } as any; + + clusterServiceMock.listUser.mockReturnValue(of([])); + + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should create component', () => { + expect(component).toBeTruthy(); + }); + + it('should initialize form with required + noClientPrefix validator', () => { + const control = component.entityForm.get('user_entity'); + + control?.setValue(''); + expect(control?.valid).toBeFalsy(); + + control?.setValue('client.test'); + expect(control?.errors?.['forbiddenClientPrefix']).toBeTruthy(); + + control?.setValue('validuser'); + expect(control?.valid).toBeTruthy(); + }); + + it('should filter entities based on fsname', fakeAsync(() => { + clusterServiceMock.listUser.mockReturnValue( + of([ + { + entity: 'client.valid', + caps: { mds: 'allow rw fsname=myfs' } + }, + { + entity: 'client.invalid', + caps: { mds: 'allow rw fsname=otherfs' } + }, + { + entity: 'osd.something', + caps: { mds: 'allow rw fsname=myfs' } + } + ]) + ); + + component.loadEntities(); + tick(); + + component.entities$.subscribe((rows) => { + expect(rows.length).toBe(1); + expect(rows[0].entity).toBe('client.valid'); + expect(rows[0].mdsCaps).toContain('fsname=myfs'); + }); + })); + + it('should not submit if form invalid', () => { + component.entityForm.get('user_entity')?.setValue(''); + + component.submitAction(); + + expect(clusterServiceMock.createUser).not.toHaveBeenCalled(); + expect(component.isSubmitting).toBeFalsy(); + }); + + it('should create entity and set auth successfully', fakeAsync(() => { + component.entityForm.get('user_entity')?.setValue('newuser'); + + clusterServiceMock.createUser.mockReturnValue(of({})); + taskWrapperServiceMock.wrapTaskAroundCall.mockReturnValue(of({})); + cephfsServiceMock.setAuth.mockReturnValue(of({})); + + const emitSpy = jest.spyOn(component.entitySelected, 'emit'); + + component.submitAction(); + tick(); + + expect(clusterServiceMock.createUser).toHaveBeenCalledWith( + expect.objectContaining({ + user_entity: 'client.newuser' + }) + ); + + expect(cephfsServiceMock.setAuth).toHaveBeenCalledWith('myfs', 'newuser', ['/', 'rwps'], false); + + expect(emitSpy).toHaveBeenCalledWith('client.newuser'); + expect(component.isSubmitting).toBeFalsy(); + expect(component.isCreatingNewEntity).toBeFalsy(); + })); + + it('should emit selected entity on updateSelection', () => { + const selection = new CdTableSelection(); + selection.selected = [{ entity: 'client.test' }]; + + const emitSpy = jest.spyOn(component.entitySelected, 'emit'); + + component.updateSelection(selection); + + expect(emitSpy).toHaveBeenCalledWith('client.test'); + }); + + it('should emit null when selection empty', () => { + const selection = new CdTableSelection(); + selection.selected = []; + + const emitSpy = jest.spyOn(component.entitySelected, 'emit'); + + component.updateSelection(selection); + + expect(emitSpy).toHaveBeenCalledWith(null); + }); + + it('should toggle create/existing states correctly', () => { + component.onExistingEntitySelected(); + expect(component.isCreatingNewEntity).toBeFalsy(); + + component.onCreateEntitySelected(); + expect(component.isCreatingNewEntity).toBeTruthy(); + }); + + it('should dismiss warnings correctly', () => { + component.onDismissCreateRequirementsWarning(); + expect(component.showCreateRequirementsWarning).toBeFalsy(); + + component.onDismissCreateCapabilitiesInfo(); + expect(component.showCreateCapabilitiesInfo).toBeFalsy(); + + component.onDismissSelectRequirementsWarning(); + expect(component.showSelectRequirementsWarning).toBeFalsy(); + + component.onDismissSelectEntityInfo(); + expect(component.showSelectEntityInfo).toBeFalsy(); + }); +}); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.ts new file mode 100644 index 00000000000..1fad704c9b5 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-entity/cephfs-mirroring-entity.component.ts @@ -0,0 +1,233 @@ +import { Component, OnInit, Output, EventEmitter, Input, inject } from '@angular/core'; +import { BehaviorSubject, Observable, of } from 'rxjs'; +import { catchError, map, switchMap, defaultIfEmpty } from 'rxjs/operators'; + +import { CdTableColumn } from '~/app/shared/models/cd-table-column'; +import { CdTableSelection } from '~/app/shared/models/cd-table-selection'; +import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context'; +import { CephfsService } from '~/app/shared/api/cephfs.service'; +import { ClusterService } from '~/app/shared/api/cluster.service'; + +import { CdFormGroup } from '~/app/shared/forms/cd-form-group'; +import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder'; +import { Validators, AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms'; +import { CdForm } from '~/app/shared/forms/cd-form'; +import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service'; +import { FinishedTask } from '~/app/shared/models/finished-task'; +import { FilesystemRow, MirroringEntityRow } from '~/app/shared/models/cephfs.model'; +import { CephAuthUser } from '~/app/shared/models/cluster.model'; + +@Component({ + selector: 'cd-cephfs-mirroring-entity', + templateUrl: './cephfs-mirroring-entity.component.html', + styleUrls: ['./cephfs-mirroring-entity.component.scss'], + standalone: false +}) +export class CephfsMirroringEntityComponent extends CdForm implements OnInit { + columns: CdTableColumn[]; + selection = new CdTableSelection(); + + subject$ = new BehaviorSubject(undefined); + entities$: Observable; + context: CdTableFetchDataContext; + capabilities = [ + { name: $localize`MDS`, permission: 'rwps' }, + { name: $localize`MON`, permission: 'rwps' }, + { name: $localize`OSD`, permission: 'rwps' } + ]; + + isCreatingNewEntity = true; + showCreateRequirementsWarning = true; + showCreateCapabilitiesInfo = true; + showSelectRequirementsWarning = true; + showSelectEntityInfo = true; + + entityForm: CdFormGroup; + + readonly userEntityHelperText = $localize`Ceph Authentication entity used by mirroring.`; + + @Input() selectedFilesystem: FilesystemRow | null = null; + @Output() entitySelected = new EventEmitter(); + isSubmitting: boolean = false; + + private cephfsService = inject(CephfsService); + private clusterService = inject(ClusterService); + private taskWrapperService = inject(TaskWrapperService); + private formBuilder = inject(CdFormBuilder); + + ngOnInit(): void { + const noClientPrefix: ValidatorFn = (control: AbstractControl): ValidationErrors | null => { + const value = (control.value ?? '').toString().trim(); + if (!value) return null; + return value.startsWith('client.') ? { forbiddenClientPrefix: true } : null; + }; + + this.entityForm = this.formBuilder.group({ + user_entity: ['', [Validators.required, noClientPrefix]] + }); + + this.columns = [ + { + name: $localize`Entity ID`, + prop: 'entity', + flexGrow: 2 + }, + { + name: $localize`MDS capabilities`, + prop: 'mdsCaps', + flexGrow: 1.5 + }, + { + name: $localize`MON capabilities`, + prop: 'monCaps', + flexGrow: 1.5 + }, + { + name: $localize`OSD capabilities`, + prop: 'osdCaps', + flexGrow: 1.5 + } + ]; + + this.entities$ = this.subject$.pipe( + switchMap(() => + this.clusterService.listUser().pipe( + switchMap((users) => { + const typedUsers = (users as CephAuthUser[]) || []; + const filteredEntities = typedUsers.filter((entity) => { + if (entity.entity?.startsWith('client.')) { + const caps = entity.caps || {}; + const mdsCaps = caps.mds || '-'; + + const fsName = this.selectedFilesystem?.name || ''; + const isValid = mdsCaps.includes(`fsname=${fsName}`); + + return isValid; + } + return false; + }); + + const rows: MirroringEntityRow[] = filteredEntities.map((entity) => { + const caps = entity.caps || {}; + const mdsCaps = caps.mds || '-'; + const monCaps = caps.mon || '-'; + const osdCaps = caps.osd || '-'; + + return { + entity: entity.entity, + mdsCaps, + monCaps, + osdCaps + }; + }); + + return of(rows); + }), + catchError(() => { + this.context?.error(); + return of([]); + }) + ) + ) + ); + + this.loadEntities(); + } + + submitAction(): void { + if (!this.entityForm.valid) { + this.entityForm.markAllAsTouched(); + return; + } + + const clientEntity = (this.entityForm.get('user_entity')?.value || '').toString().trim(); + const fullEntity = `client.${clientEntity}`; + const fsName = this.selectedFilesystem?.name; + + const payload = { + user_entity: fullEntity, + capabilities: [ + { entity: 'mds', cap: 'allow *' }, + { entity: 'mgr', cap: 'allow *' }, + { entity: 'mon', cap: 'allow *' }, + { entity: 'osd', cap: 'allow *' } + ] + }; + + this.isSubmitting = true; + + this.taskWrapperService + .wrapTaskAroundCall({ + task: new FinishedTask(`ceph-user/create`, { + userEntity: fullEntity, + fsName: fsName + }), + call: this.clusterService.createUser(payload).pipe( + map((res) => { + return { ...(res as Record), __taskCompleted: true }; + }) + ) + }) + .pipe( + defaultIfEmpty(null), + switchMap(() => { + if (fsName) { + return this.cephfsService.setAuth(fsName, clientEntity, ['/', 'rwps'], false); + } + return of(null); + }) + ) + .subscribe({ + complete: () => { + this.isSubmitting = false; + this.entityForm.reset(); + this.handleEntityCreated(fullEntity); + } + }); + } + + private handleEntityCreated(entityId: string) { + this.loadEntities(this.context); + this.entitySelected.emit(entityId); + this.isCreatingNewEntity = false; + } + + loadEntities(context?: CdTableFetchDataContext) { + this.context = context; + this.subject$.next(); + } + + updateSelection(selection: CdTableSelection) { + this.selection = selection; + const selectedRow = selection?.first(); + this.entitySelected.emit(selectedRow ? selectedRow.entity : null); + } + + onCreateEntitySelected() { + this.isCreatingNewEntity = true; + this.showCreateRequirementsWarning = true; + this.showCreateCapabilitiesInfo = true; + } + + onExistingEntitySelected() { + this.isCreatingNewEntity = false; + this.showSelectRequirementsWarning = true; + this.showSelectEntityInfo = true; + } + + onDismissCreateRequirementsWarning() { + this.showCreateRequirementsWarning = false; + } + + onDismissCreateCapabilitiesInfo() { + this.showCreateCapabilitiesInfo = false; + } + + onDismissSelectRequirementsWarning() { + this.showSelectRequirementsWarning = false; + } + + onDismissSelectEntityInfo() { + this.showSelectEntityInfo = false; + } +} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html index 029bcc3f294..5c0f99ec8a1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.html @@ -9,7 +9,7 @@
Choose mirror peer role
-

Select how the cluster will participate in the CephFS Mirroring relationship.

+

Select how the cluster will participate in the CephFS mirroring relationship.

@@ -69,16 +69,19 @@ dismissible="true" (dismissed)="showMessage = false">
-
About Remote Peer Setup
-
+
About remote peer setup
+
As a remote peer, this cluster prepares to receive mirrored data from an initiating cluster. The setup includes environment validation, enabling filesystem mirroring, creating required Ceph users, and generating a bootstrap token.
-
What happens next:
+
What happens next:
  • Environment validation
  • Ceph user creation
  • @@ -93,19 +96,21 @@ - + -
    Test 2
    + +
    -

    Test3

    + Test 3
    diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts index c24710113ea..75554600392 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-mirroring-wizard/cephfs-mirroring-wizard.component.ts @@ -9,6 +9,7 @@ import { import { WizardStepsService } from '~/app/shared/services/wizard-steps.service'; import { WizardStepModel } from '~/app/shared/models/wizard-steps'; import { FormBuilder, FormGroup } from '@angular/forms'; +import { FilesystemRow } from '~/app/shared/models/cephfs.model'; @Component({ selector: 'cd-cephfs-mirroring-wizard', templateUrl: './cephfs-mirroring-wizard.component.html', @@ -21,6 +22,8 @@ export class CephfsMirroringWizardComponent implements OnInit { description: string = $localize`Configure a new mirroring relationship between clusters`; form: FormGroup; showMessage: boolean = true; + selectedFilesystem: FilesystemRow | null = null; + selectedEntity: string | null = null; LOCAL_ROLE = LOCAL_ROLE; REMOTE_ROLE = REMOTE_ROLE; @@ -54,10 +57,25 @@ export class CephfsMirroringWizardComponent implements OnInit { const stepsData = this.wizardStepsService.steps$.value; this.steps = STEP_TITLES_MIRRORING_CONFIGURED.map((title, index) => ({ label: title, - onClick: () => this.goToStep(stepsData[index]) + onClick: () => this.goToStep(stepsData[index]), + invalid: true })); } + onFilesystemSelected(filesystem: FilesystemRow) { + this.selectedFilesystem = filesystem; + if (this.steps[1]) { + this.steps[1].invalid = !filesystem; + } + } + + onEntitySelected(entity: string) { + this.selectedEntity = entity; + if (this.steps[2]) { + this.steps[2].invalid = !entity; + } + } + goToStep(step: WizardStepModel) { if (step) { this.wizardStepsService.setCurrentStep(step); @@ -67,11 +85,17 @@ export class CephfsMirroringWizardComponent implements OnInit { onLocalRoleChange() { this.form.patchValue({ localRole: LOCAL_ROLE, remoteRole: null }); this.showMessage = false; + if (this.steps[0]) { + this.steps[0].invalid = false; + } } onRemoteRoleChange() { this.form.patchValue({ localRole: null, remoteRole: REMOTE_ROLE }); this.showMessage = true; + if (this.steps[0]) { + this.steps[0].invalid = false; + } } onSubmit() {} diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts index b6461be67ff..75a2d19746d 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts @@ -47,12 +47,12 @@ import { ModalModule, NumberModule, PlaceholderModule, + RadioModule, SelectModule, TimePickerModule, TilesModule, TreeviewModule, TabsModule, - RadioModule, NotificationModule } from 'carbon-components-angular'; @@ -62,6 +62,7 @@ import Close from '@carbon/icons/es/close/32'; import Trash from '@carbon/icons/es/trash-can/32'; import { CephfsMirroringWizardComponent } from './cephfs-mirroring-wizard/cephfs-mirroring-wizard.component'; import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/cephfs-filesystem-selector.component'; +import { CephfsMirroringEntityComponent } from './cephfs-mirroring-entity/cephfs-mirroring-entity.component'; @NgModule({ imports: [ @@ -92,6 +93,7 @@ import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/ LayoutModule, ComboBoxModule, IconModule, + RadioModule, BaseChartDirective, TabsModule, RadioModule, @@ -120,7 +122,8 @@ import { CephfsFilesystemSelectorComponent } from './cephfs-filesystem-selector/ CephfsMirroringListComponent, CephfsMirroringWizardComponent, CephfsFilesystemSelectorComponent, - CephfsMirroringErrorComponent + CephfsMirroringErrorComponent, + CephfsMirroringEntityComponent ], providers: [provideCharts(withDefaultRegisterables())] }) diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts index bbd263b66e3..2d47aceed95 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs.service.ts @@ -115,7 +115,7 @@ export class CephfsService { }); } - setAuth(fsName: string, clientId: number, caps: string[], rootSquash: boolean) { + setAuth(fsName: string, clientId: string | number, caps: string[], rootSquash: boolean) { return this.http.put(`${this.baseURL}/auth`, { fs_name: fsName, client_id: `client.${clientId}`, diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts index 6b435d6ffed..7223ac8e456 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cluster.service.ts @@ -3,6 +3,8 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; +import { CephClusterUser } from '~/app/shared/models/cluster.model'; + @Injectable({ providedIn: 'root' }) @@ -24,4 +26,12 @@ export class ClusterService { { headers: { Accept: 'application/vnd.ceph.api.v0.1+json' } } ); } + + listUser(): Observable { + return this.http.get(`${this.baseURL}/user`); + } + + createUser(payload: CephClusterUser) { + return this.http.post(`${this.baseURL}/user`, payload); + } } diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts index 2ce7a5182d4..081c2433fb1 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/datatable/table/table.component.ts @@ -1147,6 +1147,8 @@ export class TableComponent implements AfterViewInit, OnInit, OnChanges, OnDestr onDeselect(deselectedRowIndex: number) { this.model.selectRow(deselectedRowIndex, false); if (this.selectionType === 'single' || this.selectionType === 'singleRadio') { + this.selection.selected = []; + this.updateSelection.emit(this.selection); return; } this._toggleSelection(deselectedRowIndex, false); diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs.model.ts index e6db4222000..4f3d4450ce5 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs.model.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cephfs.model.ts @@ -130,3 +130,10 @@ export function mdsStateToStatus(state: string | undefined): MdsStatus { } export type DaemonResponse = Daemon[]; + +export type MirroringEntityRow = { + entity: string; + mdsCaps: string; + monCaps: string; + osdCaps: string; +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cluster.model.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cluster.model.ts new file mode 100644 index 00000000000..fc1aecebbd4 --- /dev/null +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/cluster.model.ts @@ -0,0 +1,13 @@ +export interface CephClusterUser { + [key: string]: unknown; +} + +export type CephAuthUser = { + [key: string]: unknown; + entity?: string; + caps?: { + mds?: string; + mon?: string; + osd?: string; + }; +}; diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts index 48cc978d279..001daa9829e 100644 --- a/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts +++ b/src/pybind/mgr/dashboard/frontend/src/app/shared/services/task-message.service.ts @@ -551,6 +551,10 @@ export class TaskMessageService { 'cephfs/smb/standalone/delete': this.newTaskMessage( this.commonOperations.delete, (metadata: { usersGroupsId: string }) => this.smbUsersgroups(metadata) + ), + 'ceph-user/create': this.newTaskMessage( + this.commonOperations.create, + (metadata: { userEntity: string }) => this.cephUser(metadata) ) }; @@ -726,4 +730,8 @@ export class TaskMessageService { getRunningText(task: Task) { return this._getTaskTitle(task).operation.running; } + + cephUser(metadata: { userEntity: string }) { + return $localize`Ceph user '${metadata.userEntity}'`; + } }