]> git-server-git.apps.pok.os.sepia.ceph.com Git - ceph.git/blob
1fad704c9b557d57259effe2de1adcd0736066a0
[ceph.git] /
1 import { Component, OnInit, Output, EventEmitter, Input, inject } from '@angular/core';
2 import { BehaviorSubject, Observable, of } from 'rxjs';
3 import { catchError, map, switchMap, defaultIfEmpty } from 'rxjs/operators';
4
5 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
6 import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
7 import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
8 import { CephfsService } from '~/app/shared/api/cephfs.service';
9 import { ClusterService } from '~/app/shared/api/cluster.service';
10
11 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
12 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
13 import { Validators, AbstractControl, ValidationErrors, ValidatorFn } from '@angular/forms';
14 import { CdForm } from '~/app/shared/forms/cd-form';
15 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
16 import { FinishedTask } from '~/app/shared/models/finished-task';
17 import { FilesystemRow, MirroringEntityRow } from '~/app/shared/models/cephfs.model';
18 import { CephAuthUser } from '~/app/shared/models/cluster.model';
19
20 @Component({
21   selector: 'cd-cephfs-mirroring-entity',
22   templateUrl: './cephfs-mirroring-entity.component.html',
23   styleUrls: ['./cephfs-mirroring-entity.component.scss'],
24   standalone: false
25 })
26 export class CephfsMirroringEntityComponent extends CdForm implements OnInit {
27   columns: CdTableColumn[];
28   selection = new CdTableSelection();
29
30   subject$ = new BehaviorSubject<void>(undefined);
31   entities$: Observable<MirroringEntityRow[]>;
32   context: CdTableFetchDataContext;
33   capabilities = [
34     { name: $localize`MDS`, permission: 'rwps' },
35     { name: $localize`MON`, permission: 'rwps' },
36     { name: $localize`OSD`, permission: 'rwps' }
37   ];
38
39   isCreatingNewEntity = true;
40   showCreateRequirementsWarning = true;
41   showCreateCapabilitiesInfo = true;
42   showSelectRequirementsWarning = true;
43   showSelectEntityInfo = true;
44
45   entityForm: CdFormGroup;
46
47   readonly userEntityHelperText = $localize`Ceph Authentication entity used by mirroring.`;
48
49   @Input() selectedFilesystem: FilesystemRow | null = null;
50   @Output() entitySelected = new EventEmitter<string | null>();
51   isSubmitting: boolean = false;
52
53   private cephfsService = inject(CephfsService);
54   private clusterService = inject(ClusterService);
55   private taskWrapperService = inject(TaskWrapperService);
56   private formBuilder = inject(CdFormBuilder);
57
58   ngOnInit(): void {
59     const noClientPrefix: ValidatorFn = (control: AbstractControl): ValidationErrors | null => {
60       const value = (control.value ?? '').toString().trim();
61       if (!value) return null;
62       return value.startsWith('client.') ? { forbiddenClientPrefix: true } : null;
63     };
64
65     this.entityForm = this.formBuilder.group({
66       user_entity: ['', [Validators.required, noClientPrefix]]
67     });
68
69     this.columns = [
70       {
71         name: $localize`Entity ID`,
72         prop: 'entity',
73         flexGrow: 2
74       },
75       {
76         name: $localize`MDS capabilities`,
77         prop: 'mdsCaps',
78         flexGrow: 1.5
79       },
80       {
81         name: $localize`MON capabilities`,
82         prop: 'monCaps',
83         flexGrow: 1.5
84       },
85       {
86         name: $localize`OSD capabilities`,
87         prop: 'osdCaps',
88         flexGrow: 1.5
89       }
90     ];
91
92     this.entities$ = this.subject$.pipe(
93       switchMap(() =>
94         this.clusterService.listUser().pipe(
95           switchMap((users) => {
96             const typedUsers = (users as CephAuthUser[]) || [];
97             const filteredEntities = typedUsers.filter((entity) => {
98               if (entity.entity?.startsWith('client.')) {
99                 const caps = entity.caps || {};
100                 const mdsCaps = caps.mds || '-';
101
102                 const fsName = this.selectedFilesystem?.name || '';
103                 const isValid = mdsCaps.includes(`fsname=${fsName}`);
104
105                 return isValid;
106               }
107               return false;
108             });
109
110             const rows: MirroringEntityRow[] = filteredEntities.map((entity) => {
111               const caps = entity.caps || {};
112               const mdsCaps = caps.mds || '-';
113               const monCaps = caps.mon || '-';
114               const osdCaps = caps.osd || '-';
115
116               return {
117                 entity: entity.entity,
118                 mdsCaps,
119                 monCaps,
120                 osdCaps
121               };
122             });
123
124             return of(rows);
125           }),
126           catchError(() => {
127             this.context?.error();
128             return of([]);
129           })
130         )
131       )
132     );
133
134     this.loadEntities();
135   }
136
137   submitAction(): void {
138     if (!this.entityForm.valid) {
139       this.entityForm.markAllAsTouched();
140       return;
141     }
142
143     const clientEntity = (this.entityForm.get('user_entity')?.value || '').toString().trim();
144     const fullEntity = `client.${clientEntity}`;
145     const fsName = this.selectedFilesystem?.name;
146
147     const payload = {
148       user_entity: fullEntity,
149       capabilities: [
150         { entity: 'mds', cap: 'allow *' },
151         { entity: 'mgr', cap: 'allow *' },
152         { entity: 'mon', cap: 'allow *' },
153         { entity: 'osd', cap: 'allow *' }
154       ]
155     };
156
157     this.isSubmitting = true;
158
159     this.taskWrapperService
160       .wrapTaskAroundCall({
161         task: new FinishedTask(`ceph-user/create`, {
162           userEntity: fullEntity,
163           fsName: fsName
164         }),
165         call: this.clusterService.createUser(payload).pipe(
166           map((res) => {
167             return { ...(res as Record<string, unknown>), __taskCompleted: true };
168           })
169         )
170       })
171       .pipe(
172         defaultIfEmpty(null),
173         switchMap(() => {
174           if (fsName) {
175             return this.cephfsService.setAuth(fsName, clientEntity, ['/', 'rwps'], false);
176           }
177           return of(null);
178         })
179       )
180       .subscribe({
181         complete: () => {
182           this.isSubmitting = false;
183           this.entityForm.reset();
184           this.handleEntityCreated(fullEntity);
185         }
186       });
187   }
188
189   private handleEntityCreated(entityId: string) {
190     this.loadEntities(this.context);
191     this.entitySelected.emit(entityId);
192     this.isCreatingNewEntity = false;
193   }
194
195   loadEntities(context?: CdTableFetchDataContext) {
196     this.context = context;
197     this.subject$.next();
198   }
199
200   updateSelection(selection: CdTableSelection) {
201     this.selection = selection;
202     const selectedRow = selection?.first();
203     this.entitySelected.emit(selectedRow ? selectedRow.entity : null);
204   }
205
206   onCreateEntitySelected() {
207     this.isCreatingNewEntity = true;
208     this.showCreateRequirementsWarning = true;
209     this.showCreateCapabilitiesInfo = true;
210   }
211
212   onExistingEntitySelected() {
213     this.isCreatingNewEntity = false;
214     this.showSelectRequirementsWarning = true;
215     this.showSelectEntityInfo = true;
216   }
217
218   onDismissCreateRequirementsWarning() {
219     this.showCreateRequirementsWarning = false;
220   }
221
222   onDismissCreateCapabilitiesInfo() {
223     this.showCreateCapabilitiesInfo = false;
224   }
225
226   onDismissSelectRequirementsWarning() {
227     this.showSelectRequirementsWarning = false;
228   }
229
230   onDismissSelectEntityInfo() {
231     this.showSelectEntityInfo = false;
232   }
233 }