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