]> git.proxmox.com Git - ceph.git/blob - ceph/src/pybind/mgr/dashboard/frontend/src/app/ceph/nfs/nfs-form/nfs-form.component.ts
add stop-gap to fix compat with CPUs not supporting SSE 4.1
[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 {
3 AbstractControl,
4 AsyncValidatorFn,
5 FormControl,
6 ValidationErrors,
7 Validators
8 } from '@angular/forms';
9 import { ActivatedRoute, Router } from '@angular/router';
10
11 import _ from 'lodash';
12 import { forkJoin, Observable, of } from 'rxjs';
13 import { catchError, debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
14
15 import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
16 import { Directory, NfsService } from '~/app/shared/api/nfs.service';
17 import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
18 import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
19 import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
20 import { Icons } from '~/app/shared/enum/icons.enum';
21 import { CdForm } from '~/app/shared/forms/cd-form';
22 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
23 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
24 import { CdValidators } from '~/app/shared/forms/cd-validators';
25 import { FinishedTask } from '~/app/shared/models/finished-task';
26 import { Permission } from '~/app/shared/models/permissions';
27 import { CdHttpErrorResponse } from '~/app/shared/services/api-interceptor.service';
28 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
29 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
30 import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
31
32 @Component({
33 selector: 'cd-nfs-form',
34 templateUrl: './nfs-form.component.html',
35 styleUrls: ['./nfs-form.component.scss']
36 })
37 export class NfsFormComponent extends CdForm implements OnInit {
38 @ViewChild('nfsClients', { static: true })
39 nfsClients: NfsFormClientComponent;
40
41 clients: any[] = [];
42
43 permission: Permission;
44 nfsForm: CdFormGroup;
45 isEdit = false;
46
47 cluster_id: string = null;
48 export_id: string = null;
49
50 allClusters: { cluster_id: string }[] = null;
51 icons = Icons;
52
53 allFsals: any[] = [];
54 allFsNames: any[] = null;
55 fsalAvailabilityError: string = null;
56
57 defaultAccessType = { RGW: 'RO' };
58 nfsAccessType: any[] = this.nfsService.nfsAccessType;
59 nfsSquash: any[] = Object.keys(this.nfsService.nfsSquash);
60
61 action: string;
62 resource: string;
63
64 pathDataSource = (text$: Observable<string>) => {
65 return text$.pipe(
66 debounceTime(200),
67 distinctUntilChanged(),
68 mergeMap((token: string) => this.getPathTypeahead(token)),
69 map((val: string[]) => val)
70 );
71 };
72
73 bucketDataSource = (text$: Observable<string>) => {
74 return text$.pipe(
75 debounceTime(200),
76 distinctUntilChanged(),
77 mergeMap((token: string) => this.getBucketTypeahead(token))
78 );
79 };
80
81 constructor(
82 private authStorageService: AuthStorageService,
83 private nfsService: NfsService,
84 private route: ActivatedRoute,
85 private router: Router,
86 private rgwBucketService: RgwBucketService,
87 private rgwSiteService: RgwSiteService,
88 private formBuilder: CdFormBuilder,
89 private taskWrapper: TaskWrapperService,
90 private cdRef: ChangeDetectorRef,
91 public actionLabels: ActionLabelsI18n
92 ) {
93 super();
94 this.permission = this.authStorageService.getPermissions().pool;
95 this.resource = $localize`NFS export`;
96 this.createForm();
97 }
98
99 ngOnInit() {
100 const promises: Observable<any>[] = [
101 this.nfsService.listClusters(),
102 this.nfsService.fsals(),
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 this.nfsForm.get('cluster_id').disable();
120 } else {
121 this.action = this.actionLabels.CREATE;
122 this.getData(promises);
123 }
124 }
125
126 getData(promises: Observable<any>[]) {
127 forkJoin(promises).subscribe((data: any[]) => {
128 this.resolveClusters(data[0]);
129 this.resolveFsals(data[1]);
130 this.resolveFilesystems(data[2]);
131 if (data[3]) {
132 this.resolveModel(data[3]);
133 }
134
135 this.loadingReady();
136 });
137 }
138
139 createForm() {
140 this.nfsForm = new CdFormGroup({
141 cluster_id: new FormControl('', {
142 validators: [Validators.required]
143 }),
144 fsal: new CdFormGroup({
145 name: new FormControl('', {
146 validators: [Validators.required]
147 }),
148 fs_name: new FormControl('', {
149 validators: [
150 CdValidators.requiredIf({
151 name: 'CEPH'
152 })
153 ]
154 })
155 }),
156 path: new FormControl('/'),
157 protocolNfsv4: new FormControl(true),
158 pseudo: new FormControl('', {
159 validators: [
160 CdValidators.requiredIf({ protocolNfsv4: true }),
161 Validators.pattern('^/[^><|&()]*$')
162 ]
163 }),
164 access_type: new FormControl('RW'),
165 squash: new FormControl(this.nfsSquash[0]),
166 transportUDP: new FormControl(true, {
167 validators: [
168 CdValidators.requiredIf({ transportTCP: false }, (value: boolean) => {
169 return !value;
170 })
171 ]
172 }),
173 transportTCP: new FormControl(true, {
174 validators: [
175 CdValidators.requiredIf({ transportUDP: false }, (value: boolean) => {
176 return !value;
177 })
178 ]
179 }),
180 clients: this.formBuilder.array([]),
181 security_label: new FormControl(false),
182 sec_label_xattr: new FormControl(
183 'security.selinux',
184 CdValidators.requiredIf({ security_label: true, 'fsal.name': 'CEPH' })
185 )
186 });
187 }
188
189 resolveModel(res: any) {
190 if (res.fsal.name === 'CEPH') {
191 res.sec_label_xattr = res.fsal.sec_label_xattr;
192 }
193
194 res.protocolNfsv4 = res.protocols.indexOf(4) !== -1;
195 delete res.protocols;
196
197 res.transportTCP = res.transports.indexOf('TCP') !== -1;
198 res.transportUDP = res.transports.indexOf('UDP') !== -1;
199 delete res.transports;
200
201 Object.entries(this.nfsService.nfsSquash).forEach(([key, value]) => {
202 if (value.includes(res.squash)) {
203 res.squash = key;
204 }
205 });
206
207 res.clients.forEach((client: any) => {
208 let addressStr = '';
209 client.addresses.forEach((address: string) => {
210 addressStr += address + ', ';
211 });
212 if (addressStr.length >= 2) {
213 addressStr = addressStr.substring(0, addressStr.length - 2);
214 }
215 client.addresses = addressStr;
216 });
217
218 this.nfsForm.patchValue(res);
219 this.setPathValidation();
220 this.clients = res.clients;
221 }
222
223 resolveClusters(clusters: string[]) {
224 this.allClusters = [];
225 for (const cluster of clusters) {
226 this.allClusters.push({ cluster_id: cluster });
227 }
228 if (!this.isEdit && this.allClusters.length > 0) {
229 this.nfsForm.get('cluster_id').setValue(this.allClusters[0].cluster_id);
230 }
231 }
232
233 resolveFsals(res: string[]) {
234 res.forEach((fsal) => {
235 const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => {
236 return fsal === currentFsalItem.value;
237 });
238
239 if (_.isObjectLike(fsalItem)) {
240 this.allFsals.push(fsalItem);
241 }
242 });
243 if (!this.isEdit && this.allFsals.length > 0) {
244 this.nfsForm.patchValue({
245 fsal: {
246 name: this.allFsals[0].value
247 }
248 });
249 }
250 }
251
252 resolveFilesystems(filesystems: any[]) {
253 this.allFsNames = filesystems;
254 if (!this.isEdit && filesystems.length > 0) {
255 this.nfsForm.patchValue({
256 fsal: {
257 fs_name: filesystems[0].name
258 }
259 });
260 }
261 }
262
263 fsalChangeHandler() {
264 this.setPathValidation();
265 const fsalValue = this.nfsForm.getValue('name');
266 const checkAvailability =
267 fsalValue === 'RGW'
268 ? this.rgwSiteService.get('realms').pipe(
269 mergeMap((realms: string[]) =>
270 realms.length === 0
271 ? of(true)
272 : this.rgwSiteService.isDefaultRealm().pipe(
273 mergeMap((isDefaultRealm) => {
274 if (!isDefaultRealm) {
275 throw new Error('Selected realm is not the default.');
276 }
277 return of(true);
278 })
279 )
280 )
281 )
282 : this.nfsService.filesystems();
283
284 checkAvailability.subscribe({
285 next: () => {
286 this.setFsalAvailability(fsalValue, true);
287 if (!this.isEdit) {
288 this.nfsForm.patchValue({
289 path: fsalValue === 'RGW' ? '' : '/',
290 pseudo: this.generatePseudo(),
291 access_type: this.updateAccessType()
292 });
293 }
294
295 this.cdRef.detectChanges();
296 },
297 error: (error) => {
298 this.setFsalAvailability(fsalValue, false, error);
299 this.nfsForm.get('name').setValue('');
300 }
301 });
302 }
303
304 private setFsalAvailability(fsalValue: string, available: boolean, errorMessage: string = '') {
305 this.allFsals = this.allFsals.map((fsalItem: NfsFSAbstractionLayer) => {
306 if (fsalItem.value === fsalValue) {
307 fsalItem.disabled = !available;
308
309 this.fsalAvailabilityError = fsalItem.disabled
310 ? $localize`${fsalItem.descr} backend is not available. ${errorMessage}`
311 : null;
312 }
313 return fsalItem;
314 });
315 }
316
317 accessTypeChangeHandler() {
318 const name = this.nfsForm.getValue('name');
319 const accessType = this.nfsForm.getValue('access_type');
320 this.defaultAccessType[name] = accessType;
321 }
322
323 setPathValidation() {
324 const path = this.nfsForm.get('path');
325 path.setValidators([Validators.required]);
326 if (this.nfsForm.getValue('name') === 'RGW') {
327 path.setAsyncValidators([CdValidators.bucketExistence(true, this.rgwBucketService)]);
328 } else {
329 path.setAsyncValidators([this.pathExistence(true)]);
330 }
331
332 if (this.isEdit) {
333 path.markAsDirty();
334 }
335 }
336
337 getAccessTypeHelp(accessType: string) {
338 const accessTypeItem = this.nfsAccessType.find((currentAccessTypeItem) => {
339 if (accessType === currentAccessTypeItem.value) {
340 return currentAccessTypeItem;
341 }
342 });
343 return _.isObjectLike(accessTypeItem) ? accessTypeItem.help : '';
344 }
345
346 getId() {
347 if (
348 _.isString(this.nfsForm.getValue('cluster_id')) &&
349 _.isString(this.nfsForm.getValue('path'))
350 ) {
351 return this.nfsForm.getValue('cluster_id') + ':' + this.nfsForm.getValue('path');
352 }
353 return '';
354 }
355
356 private getPathTypeahead(path: any) {
357 if (!_.isString(path) || path === '/') {
358 return of([]);
359 }
360
361 const fsName = this.nfsForm.getValue('fsal').fs_name;
362 return this.nfsService.lsDir(fsName, path).pipe(
363 map((result: Directory) =>
364 result.paths.filter((dirName: string) => dirName.toLowerCase().includes(path)).slice(0, 15)
365 ),
366 catchError(() => of([$localize`Error while retrieving paths.`]))
367 );
368 }
369
370 pathChangeHandler() {
371 if (!this.isEdit) {
372 this.nfsForm.patchValue({
373 pseudo: this.generatePseudo()
374 });
375 }
376 }
377
378 private getBucketTypeahead(path: string): Observable<any> {
379 if (_.isString(path) && path !== '/' && path !== '') {
380 return this.rgwBucketService.list().pipe(
381 map((bucketList) =>
382 bucketList
383 .filter((bucketName: string) => bucketName.toLowerCase().includes(path))
384 .slice(0, 15)
385 ),
386 catchError(() => of([$localize`Error while retrieving bucket names.`]))
387 );
388 } else {
389 return of([]);
390 }
391 }
392
393 private generatePseudo() {
394 let newPseudo = this.nfsForm.getValue('pseudo');
395 if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
396 newPseudo = undefined;
397 if (this.nfsForm.getValue('fsal') === 'CEPH') {
398 newPseudo = '/cephfs';
399 if (_.isString(this.nfsForm.getValue('path'))) {
400 newPseudo += this.nfsForm.getValue('path');
401 }
402 }
403 }
404 return newPseudo;
405 }
406
407 private updateAccessType() {
408 const name = this.nfsForm.getValue('name');
409 let accessType = this.defaultAccessType[name];
410
411 if (!accessType) {
412 accessType = 'RW';
413 }
414
415 return accessType;
416 }
417
418 submitAction() {
419 let action: Observable<any>;
420 const requestModel = this.buildRequest();
421
422 if (this.isEdit) {
423 action = this.taskWrapper.wrapTaskAroundCall({
424 task: new FinishedTask('nfs/edit', {
425 cluster_id: this.cluster_id,
426 export_id: _.parseInt(this.export_id)
427 }),
428 call: this.nfsService.update(this.cluster_id, _.parseInt(this.export_id), requestModel)
429 });
430 } else {
431 // Create
432 action = this.taskWrapper.wrapTaskAroundCall({
433 task: new FinishedTask('nfs/create', {
434 path: requestModel.path,
435 fsal: requestModel.fsal,
436 cluster_id: requestModel.cluster_id
437 }),
438 call: this.nfsService.create(requestModel)
439 });
440 }
441
442 action.subscribe({
443 error: (errorResponse: CdHttpErrorResponse) => this.setFormErrors(errorResponse),
444 complete: () => this.router.navigate(['/nfs'])
445 });
446 }
447
448 private setFormErrors(errorResponse: CdHttpErrorResponse) {
449 if (
450 errorResponse.error.detail &&
451 errorResponse.error.detail
452 .toString()
453 .includes(`Pseudo ${this.nfsForm.getValue('pseudo')} is already in use`)
454 ) {
455 this.nfsForm.get('pseudo').setErrors({ pseudoAlreadyExists: true });
456 }
457 this.nfsForm.setErrors({ cdSubmitButton: true });
458 }
459
460 private buildRequest() {
461 const requestModel: any = _.cloneDeep(this.nfsForm.value);
462
463 if (this.isEdit) {
464 requestModel.export_id = _.parseInt(this.export_id);
465 }
466
467 if (requestModel.fsal.name === 'RGW') {
468 delete requestModel.fsal.fs_name;
469 }
470
471 requestModel.protocols = [];
472 if (requestModel.protocolNfsv4) {
473 requestModel.protocols.push(4);
474 } else {
475 requestModel.pseudo = null;
476 }
477 delete requestModel.protocolNfsv4;
478
479 requestModel.transports = [];
480 if (requestModel.transportTCP) {
481 requestModel.transports.push('TCP');
482 }
483 delete requestModel.transportTCP;
484 if (requestModel.transportUDP) {
485 requestModel.transports.push('UDP');
486 }
487 delete requestModel.transportUDP;
488
489 requestModel.clients.forEach((client: any) => {
490 if (_.isString(client.addresses)) {
491 client.addresses = _(client.addresses)
492 .split(/[ ,]+/)
493 .uniq()
494 .filter((address) => address !== '')
495 .value();
496 } else {
497 client.addresses = [];
498 }
499
500 if (!client.squash) {
501 client.squash = requestModel.squash;
502 }
503
504 if (!client.access_type) {
505 client.access_type = requestModel.access_type;
506 }
507 });
508
509 if (requestModel.security_label === false || requestModel.fsal.name === 'RGW') {
510 requestModel.fsal.sec_label_xattr = null;
511 } else {
512 requestModel.fsal.sec_label_xattr = requestModel.sec_label_xattr;
513 }
514 delete requestModel.sec_label_xattr;
515
516 return requestModel;
517 }
518
519 private pathExistence(requiredExistenceResult: boolean): AsyncValidatorFn {
520 return (control: AbstractControl): Observable<ValidationErrors | null> => {
521 if (control.pristine || !control.value) {
522 return of({ required: true });
523 }
524 const fsName = this.nfsForm.getValue('fsal').fs_name;
525 return this.nfsService.lsDir(fsName, control.value).pipe(
526 map((directory: Directory) =>
527 directory.paths.includes(control.value) === requiredExistenceResult
528 ? null
529 : { pathNameNotAllowed: true }
530 ),
531 catchError(() => of({ pathNameNotAllowed: true }))
532 );
533 };
534 }
535 }