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