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