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