]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
import ceph pacific 16.2.5
[ceph.git] / ceph / src / pybind / mgr / dashboard / frontend / src / app / ceph / nfs / nfs-form / nfs-form.component.ts
1 import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
2 import { FormControl, Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
4
5 import _ from 'lodash';
6 import { forkJoin, Observable, of } from 'rxjs';
7 import { debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
8
9 import { NfsService } from '~/app/shared/api/nfs.service';
10 import { RgwUserService } from '~/app/shared/api/rgw-user.service';
11 import { SelectMessages } from '~/app/shared/components/select/select-messages.model';
12 import { SelectOption } from '~/app/shared/components/select/select-option.model';
13 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
14 import { Icons } from '~/app/shared/enum/icons.enum';
15 import { CdForm } from '~/app/shared/forms/cd-form';
16 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
17 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
18 import { CdValidators } from '~/app/shared/forms/cd-validators';
19 import { FinishedTask } from '~/app/shared/models/finished-task';
20 import { Permission } from '~/app/shared/models/permissions';
21 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
22 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
23 import { NFSClusterType } from '../nfs-cluster-type.enum';
24 import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
25
26 @Component({
27 selector: 'cd-nfs-form',
28 templateUrl: './nfs-form.component.html',
29 styleUrls: ['./nfs-form.component.scss']
30 })
31 export class NfsFormComponent extends CdForm implements OnInit {
32 @ViewChild('nfsClients', { static: true })
33 nfsClients: NfsFormClientComponent;
34
35 clients: any[] = [];
36
37 permission: Permission;
38 nfsForm: CdFormGroup;
39 isEdit = false;
40
41 cluster_id: string = null;
42 clusterType: string = null;
43 export_id: string = null;
44
45 isNewDirectory = false;
46 isNewBucket = false;
47 isDefaultCluster = false;
48
49 allClusters: { cluster_id: string; cluster_type: string }[] = null;
50 allDaemons = {};
51 icons = Icons;
52
53 allFsals: any[] = [];
54 allRgwUsers: any[] = [];
55 allCephxClients: any[] = null;
56 allFsNames: any[] = null;
57
58 defaultAccessType = { RGW: 'RO' };
59 nfsAccessType: any[] = this.nfsService.nfsAccessType;
60 nfsSquash: any[] = this.nfsService.nfsSquash;
61
62 action: string;
63 resource: string;
64
65 daemonsSelections: SelectOption[] = [];
66 daemonsMessages = new SelectMessages({ noOptions: $localize`There are no daemons available.` });
67
68 pathDataSource = (text$: Observable<string>) => {
69 return text$.pipe(
70 debounceTime(200),
71 distinctUntilChanged(),
72 mergeMap((token: string) => this.getPathTypeahead(token)),
73 map((val: any) => val.paths)
74 );
75 };
76
77 bucketDataSource = (text$: Observable<string>) => {
78 return text$.pipe(
79 debounceTime(200),
80 distinctUntilChanged(),
81 mergeMap((token: string) => this.getBucketTypeahead(token))
82 );
83 };
84
85 constructor(
86 private authStorageService: AuthStorageService,
87 private nfsService: NfsService,
88 private route: ActivatedRoute,
89 private router: Router,
90 private rgwUserService: RgwUserService,
91 private formBuilder: CdFormBuilder,
92 private taskWrapper: TaskWrapperService,
93 private cdRef: ChangeDetectorRef,
94 public actionLabels: ActionLabelsI18n
95 ) {
96 super();
97 this.permission = this.authStorageService.getPermissions().pool;
98 this.resource = $localize`NFS export`;
99 this.createForm();
100 }
101
102 ngOnInit() {
103 const promises: Observable<any>[] = [
104 this.nfsService.daemon(),
105 this.nfsService.fsals(),
106 this.nfsService.clients(),
107 this.nfsService.filesystems()
108 ];
109
110 if (this.router.url.startsWith('/nfs/edit')) {
111 this.isEdit = true;
112 }
113
114 if (this.isEdit) {
115 this.action = this.actionLabels.EDIT;
116 this.route.params.subscribe((params: { cluster_id: string; export_id: string }) => {
117 this.cluster_id = decodeURIComponent(params.cluster_id);
118 this.export_id = decodeURIComponent(params.export_id);
119 promises.push(this.nfsService.get(this.cluster_id, this.export_id));
120
121 this.getData(promises);
122 });
123 this.nfsForm.get('cluster_id').disable();
124 } else {
125 this.action = this.actionLabels.CREATE;
126 this.getData(promises);
127 }
128 }
129
130 getData(promises: Observable<any>[]) {
131 forkJoin(promises).subscribe((data: any[]) => {
132 this.resolveDaemons(data[0]);
133 this.resolveFsals(data[1]);
134 this.resolveClients(data[2]);
135 this.resolveFilesystems(data[3]);
136 if (data[4]) {
137 this.resolveModel(data[4]);
138 }
139
140 this.loadingReady();
141 });
142 }
143
144 createForm() {
145 this.nfsForm = new CdFormGroup({
146 cluster_id: new FormControl('', {
147 validators: [Validators.required]
148 }),
149 daemons: new FormControl([]),
150 fsal: new CdFormGroup({
151 name: new FormControl('', {
152 validators: [Validators.required]
153 }),
154 user_id: new FormControl('', {
155 validators: [
156 CdValidators.requiredIf({
157 name: 'CEPH'
158 })
159 ]
160 }),
161 fs_name: new FormControl('', {
162 validators: [
163 CdValidators.requiredIf({
164 name: 'CEPH'
165 })
166 ]
167 }),
168 rgw_user_id: new FormControl('', {
169 validators: [
170 CdValidators.requiredIf({
171 name: 'RGW'
172 })
173 ]
174 })
175 }),
176 path: new FormControl(''),
177 protocolNfsv3: new FormControl(false, {
178 validators: [
179 CdValidators.requiredIf({ protocolNfsv4: false }, (value: boolean) => {
180 return !value;
181 })
182 ]
183 }),
184 protocolNfsv4: new FormControl(true, {
185 validators: [
186 CdValidators.requiredIf({ protocolNfsv3: false }, (value: boolean) => {
187 return !value;
188 })
189 ]
190 }),
191 tag: new FormControl(''),
192 pseudo: new FormControl('', {
193 validators: [
194 CdValidators.requiredIf({ protocolNfsv4: true }),
195 Validators.pattern('^/[^><|&()]*$')
196 ]
197 }),
198 access_type: new FormControl('RW', {
199 validators: [Validators.required]
200 }),
201 squash: new FormControl('', {
202 validators: [Validators.required]
203 }),
204 transportUDP: new FormControl(true, {
205 validators: [
206 CdValidators.requiredIf({ transportTCP: false }, (value: boolean) => {
207 return !value;
208 })
209 ]
210 }),
211 transportTCP: new FormControl(true, {
212 validators: [
213 CdValidators.requiredIf({ transportUDP: false }, (value: boolean) => {
214 return !value;
215 })
216 ]
217 }),
218 clients: this.formBuilder.array([]),
219 security_label: new FormControl(false),
220 sec_label_xattr: new FormControl(
221 'security.selinux',
222 CdValidators.requiredIf({ security_label: true, 'fsal.name': 'CEPH' })
223 )
224 });
225 }
226
227 resolveModel(res: any) {
228 if (res.fsal.name === 'CEPH') {
229 res.sec_label_xattr = res.fsal.sec_label_xattr;
230 }
231
232 if (this.clusterType === NFSClusterType.user) {
233 this.daemonsSelections = _.map(
234 this.allDaemons[res.cluster_id],
235 (daemon) => new SelectOption(res.daemons.indexOf(daemon) !== -1, daemon, '')
236 );
237 this.daemonsSelections = [...this.daemonsSelections];
238 }
239
240 res.protocolNfsv3 = res.protocols.indexOf(3) !== -1;
241 res.protocolNfsv4 = res.protocols.indexOf(4) !== -1;
242 delete res.protocols;
243
244 res.transportTCP = res.transports.indexOf('TCP') !== -1;
245 res.transportUDP = res.transports.indexOf('UDP') !== -1;
246 delete res.transports;
247
248 res.clients.forEach((client: any) => {
249 let addressStr = '';
250 client.addresses.forEach((address: string) => {
251 addressStr += address + ', ';
252 });
253 if (addressStr.length >= 2) {
254 addressStr = addressStr.substring(0, addressStr.length - 2);
255 }
256 client.addresses = addressStr;
257 });
258
259 this.nfsForm.patchValue(res);
260 this.setPathValidation();
261 this.clients = res.clients;
262 }
263
264 resolveDaemons(daemons: Record<string, any>) {
265 daemons = _.sortBy(daemons, ['daemon_id']);
266 const clusters = _.groupBy(daemons, 'cluster_id');
267
268 this.allClusters = [];
269 _.forIn(clusters, (cluster, cluster_id) => {
270 this.allClusters.push({ cluster_id: cluster_id, cluster_type: cluster[0].cluster_type });
271 this.allDaemons[cluster_id] = [];
272 });
273
274 _.forEach(daemons, (daemon) => {
275 this.allDaemons[daemon.cluster_id].push(daemon.daemon_id);
276 });
277
278 if (this.isEdit) {
279 this.clusterType = _.find(this.allClusters, { cluster_id: this.cluster_id })?.cluster_type;
280 }
281
282 const hasOneCluster = _.isArray(this.allClusters) && this.allClusters.length === 1;
283 this.isDefaultCluster = hasOneCluster && this.allClusters[0].cluster_id === '_default_';
284 if (hasOneCluster) {
285 this.nfsForm.patchValue({
286 cluster_id: this.allClusters[0].cluster_id
287 });
288 this.onClusterChange();
289 }
290 }
291
292 resolveFsals(res: string[]) {
293 res.forEach((fsal) => {
294 const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => {
295 return fsal === currentFsalItem.value;
296 });
297
298 if (_.isObjectLike(fsalItem)) {
299 this.allFsals.push(fsalItem);
300 if (fsalItem.value === 'RGW') {
301 this.rgwUserService.list().subscribe((result: any) => {
302 result.forEach((user: Record<string, any>) => {
303 if (user.suspended === 0 && user.keys.length > 0) {
304 const userId = user.tenant ? `${user.tenant}$${user.user_id}` : user.user_id;
305 this.allRgwUsers.push(userId);
306 }
307 });
308 });
309 }
310 }
311 });
312
313 if (this.allFsals.length === 1 && _.isUndefined(this.nfsForm.getValue('fsal'))) {
314 this.nfsForm.patchValue({
315 fsal: this.allFsals[0]
316 });
317 }
318 }
319
320 resolveClients(clients: any[]) {
321 this.allCephxClients = clients;
322 }
323
324 resolveFilesystems(filesystems: any[]) {
325 this.allFsNames = filesystems;
326 if (filesystems.length === 1) {
327 this.nfsForm.patchValue({
328 fsal: {
329 fs_name: filesystems[0].name
330 }
331 });
332 }
333 }
334
335 fsalChangeHandler() {
336 this.nfsForm.patchValue({
337 tag: this._generateTag(),
338 pseudo: this._generatePseudo(),
339 access_type: this._updateAccessType()
340 });
341
342 this.setPathValidation();
343
344 this.cdRef.detectChanges();
345 }
346
347 accessTypeChangeHandler() {
348 const name = this.nfsForm.getValue('name');
349 const accessType = this.nfsForm.getValue('access_type');
350 this.defaultAccessType[name] = accessType;
351 }
352
353 setPathValidation() {
354 if (this.nfsForm.getValue('name') === 'RGW') {
355 this.nfsForm
356 .get('path')
357 .setValidators([Validators.required, Validators.pattern('^(/|[^/><|&()#?]+)$')]);
358 } else {
359 this.nfsForm
360 .get('path')
361 .setValidators([Validators.required, Validators.pattern('^/[^><|&()?]*$')]);
362 }
363 }
364
365 rgwUserIdChangeHandler() {
366 this.nfsForm.patchValue({
367 pseudo: this._generatePseudo()
368 });
369 }
370
371 getAccessTypeHelp(accessType: string) {
372 const accessTypeItem = this.nfsAccessType.find((currentAccessTypeItem) => {
373 if (accessType === currentAccessTypeItem.value) {
374 return currentAccessTypeItem;
375 }
376 });
377 return _.isObjectLike(accessTypeItem) ? accessTypeItem.help : '';
378 }
379
380 getId() {
381 if (
382 _.isString(this.nfsForm.getValue('cluster_id')) &&
383 _.isString(this.nfsForm.getValue('path'))
384 ) {
385 return this.nfsForm.getValue('cluster_id') + ':' + this.nfsForm.getValue('path');
386 }
387 return '';
388 }
389
390 getPathTypeahead(path: any) {
391 if (!_.isString(path) || path === '/') {
392 return of([]);
393 }
394
395 const fsName = this.nfsForm.getValue('fsal').fs_name;
396 return this.nfsService.lsDir(fsName, path);
397 }
398
399 pathChangeHandler() {
400 this.nfsForm.patchValue({
401 pseudo: this._generatePseudo()
402 });
403
404 const path = this.nfsForm.getValue('path');
405 this.getPathTypeahead(path).subscribe((res: any) => {
406 this.isNewDirectory = path !== '/' && res.paths.indexOf(path) === -1;
407 });
408 }
409
410 bucketChangeHandler() {
411 this.nfsForm.patchValue({
412 tag: this._generateTag(),
413 pseudo: this._generatePseudo()
414 });
415
416 const bucket = this.nfsForm.getValue('path');
417 this.getBucketTypeahead(bucket).subscribe((res: any) => {
418 this.isNewBucket = bucket !== '' && res.indexOf(bucket) === -1;
419 });
420 }
421
422 getBucketTypeahead(path: string): Observable<any> {
423 const rgwUserId = this.nfsForm.getValue('rgw_user_id');
424
425 if (_.isString(rgwUserId) && _.isString(path) && path !== '/' && path !== '') {
426 return this.nfsService.buckets(rgwUserId);
427 } else {
428 return of([]);
429 }
430 }
431
432 _generateTag() {
433 let newTag = this.nfsForm.getValue('tag');
434 if (!this.nfsForm.get('tag').dirty) {
435 newTag = undefined;
436 if (this.nfsForm.getValue('fsal') === 'RGW') {
437 newTag = this.nfsForm.getValue('path');
438 }
439 }
440 return newTag;
441 }
442
443 _generatePseudo() {
444 let newPseudo = this.nfsForm.getValue('pseudo');
445 if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
446 newPseudo = undefined;
447 if (this.nfsForm.getValue('fsal') === 'CEPH') {
448 newPseudo = '/cephfs';
449 if (_.isString(this.nfsForm.getValue('path'))) {
450 newPseudo += this.nfsForm.getValue('path');
451 }
452 } else if (this.nfsForm.getValue('fsal') === 'RGW') {
453 if (_.isString(this.nfsForm.getValue('rgw_user_id'))) {
454 newPseudo = '/' + this.nfsForm.getValue('rgw_user_id');
455 if (_.isString(this.nfsForm.getValue('path'))) {
456 newPseudo += '/' + this.nfsForm.getValue('path');
457 }
458 }
459 }
460 }
461 return newPseudo;
462 }
463
464 _updateAccessType() {
465 const name = this.nfsForm.getValue('name');
466 let accessType = this.defaultAccessType[name];
467
468 if (!accessType) {
469 accessType = 'RW';
470 }
471
472 return accessType;
473 }
474
475 onClusterChange() {
476 const cluster_id = this.nfsForm.getValue('cluster_id');
477 this.clusterType = _.find(this.allClusters, { cluster_id: cluster_id })?.cluster_type;
478 if (this.clusterType === NFSClusterType.user) {
479 this.daemonsSelections = _.map(
480 this.allDaemons[cluster_id],
481 (daemon) => new SelectOption(false, daemon, '')
482 );
483 this.daemonsSelections = [...this.daemonsSelections];
484 } else {
485 this.daemonsSelections = [];
486 }
487 this.nfsForm.patchValue({ daemons: [] });
488 }
489
490 removeDaemon(index: number, daemon: string) {
491 this.daemonsSelections.forEach((value) => {
492 if (value.name === daemon) {
493 value.selected = false;
494 }
495 });
496
497 const daemons = this.nfsForm.get('daemons');
498 daemons.value.splice(index, 1);
499 daemons.setValue(daemons.value);
500
501 return false;
502 }
503
504 onDaemonSelection() {
505 this.nfsForm.get('daemons').setValue(this.nfsForm.getValue('daemons'));
506 }
507
508 onToggleAllDaemonsSelection() {
509 const cluster_id = this.nfsForm.getValue('cluster_id');
510 const daemons =
511 this.nfsForm.getValue('daemons').length === 0 ? this.allDaemons[cluster_id] : [];
512 this.nfsForm.patchValue({ daemons: daemons });
513 }
514
515 submitAction() {
516 let action: Observable<any>;
517 const requestModel = this._buildRequest();
518
519 if (this.isEdit) {
520 action = this.taskWrapper.wrapTaskAroundCall({
521 task: new FinishedTask('nfs/edit', {
522 cluster_id: this.cluster_id,
523 export_id: this.export_id
524 }),
525 call: this.nfsService.update(this.cluster_id, this.export_id, requestModel)
526 });
527 } else {
528 // Create
529 action = this.taskWrapper.wrapTaskAroundCall({
530 task: new FinishedTask('nfs/create', {
531 path: requestModel.path,
532 fsal: requestModel.fsal,
533 cluster_id: requestModel.cluster_id
534 }),
535 call: this.nfsService.create(requestModel)
536 });
537 }
538
539 action.subscribe({
540 error: () => this.nfsForm.setErrors({ cdSubmitButton: true }),
541 complete: () => this.router.navigate(['/nfs'])
542 });
543 }
544
545 _buildRequest() {
546 const requestModel: any = _.cloneDeep(this.nfsForm.value);
547
548 if (_.isUndefined(requestModel.tag) || requestModel.tag === '') {
549 requestModel.tag = null;
550 }
551
552 if (this.isEdit) {
553 requestModel.export_id = this.export_id;
554 }
555
556 if (requestModel.fsal.name === 'CEPH') {
557 delete requestModel.fsal.rgw_user_id;
558 } else {
559 delete requestModel.fsal.fs_name;
560 delete requestModel.fsal.user_id;
561 }
562
563 requestModel.protocols = [];
564 if (requestModel.protocolNfsv3) {
565 requestModel.protocols.push(3);
566 } else {
567 requestModel.tag = null;
568 }
569 delete requestModel.protocolNfsv3;
570 if (requestModel.protocolNfsv4) {
571 requestModel.protocols.push(4);
572 } else {
573 requestModel.pseudo = null;
574 }
575 delete requestModel.protocolNfsv4;
576
577 requestModel.transports = [];
578 if (requestModel.transportTCP) {
579 requestModel.transports.push('TCP');
580 }
581 delete requestModel.transportTCP;
582 if (requestModel.transportUDP) {
583 requestModel.transports.push('UDP');
584 }
585 delete requestModel.transportUDP;
586
587 requestModel.clients.forEach((client: any) => {
588 if (_.isString(client.addresses)) {
589 client.addresses = _(client.addresses)
590 .split(/[ ,]+/)
591 .uniq()
592 .filter((address) => address !== '')
593 .value();
594 } else {
595 client.addresses = [];
596 }
597
598 if (!client.squash) {
599 client.squash = requestModel.squash;
600 }
601
602 if (!client.access_type) {
603 client.access_type = requestModel.access_type;
604 }
605 });
606
607 if (requestModel.security_label === false || requestModel.fsal.name === 'RGW') {
608 requestModel.fsal.sec_label_xattr = null;
609 } else {
610 requestModel.fsal.sec_label_xattr = requestModel.sec_label_xattr;
611 }
612 delete requestModel.sec_label_xattr;
613
614 return requestModel;
615 }
616 }