'rgw daemons': { url: '#/rgw/daemon', id: 'cd-rgw-daemon-list' },
// CephFS
- cephfs: { url: '#/cephfs', id: 'cd-cephfs-list' },
- 'create cephfs': { url: '#/cephfs/create', id: 'cd-cephfs-form' }
+ cephfs: { url: '#/cephfs/fs', id: 'cd-cephfs-list' },
+ 'create cephfs': { url: '#/cephfs/fs/create', id: 'cd-cephfs-form' }
};
}
beforeEach(() => {
cy.login();
- nfsExport.navigateTo();
});
describe('breadcrumb test', () => {
it('should open and show breadcrumb', () => {
+ nfsExport.navigateTo('rgw_index');
nfsExport.expectBreadcrumbText('NFS');
});
});
buckets.navigateTo('create');
buckets.create(bucketName, 'dashboard');
- nfsExport.navigateTo();
+ nfsExport.navigateTo('rgw_index');
nfsExport.existTableCell(rgwPseudo, false);
- nfsExport.navigateTo('create');
+ nfsExport.navigateTo('rgw_create');
nfsExport.create(backends[1], squash, client, rgwPseudo, bucketName);
nfsExport.existTableCell(rgwPseudo);
});
// @TODO: uncomment this when a CephFS volume can be created through Dashboard.
// it('should create a nfs-export with CephFS backend', () => {
- // nfsExport.navigateTo();
+ // nfsExport.navigateTo('cephfs_index');
// nfsExport.existTableCell(fsPseudo, false);
- // nfsExport.navigateTo('create');
+ // nfsExport.navigateTo('cephfs_create');
// nfsExport.create(backends[0], squash, client, fsPseudo);
// nfsExport.existTableCell(fsPseudo);
// });
it('should show Clients', () => {
+ nfsExport.navigateTo('rgw_index');
nfsExport.clickTab('cd-nfs-details', rgwPseudo, 'Clients (1)');
cy.get('cd-nfs-details').within(() => {
nfsExport.getTableCount('total').should('be.gte', 0);
});
it('should edit an export', () => {
- nfsExport.editExport(rgwPseudo, editPseudo);
+ nfsExport.editExport(rgwPseudo, editPseudo, 'rgw_index');
nfsExport.existTableCell(editPseudo);
});
it('should delete exports and bucket', () => {
+ nfsExport.navigateTo('rgw_index');
nfsExport.delete(editPseudo);
buckets.navigateTo();
/* tslint:enable*/
const pages = {
- index: { url: '#/nfs', id: 'cd-nfs-list' },
- create: { url: '#/nfs/create', id: 'cd-nfs-form' }
+ cephfs_index: { url: '#cephfs/nfs', id: 'cd-nfs-list' },
+ cephfs_create: { url: '#cephfs/nfs/create', id: 'cd-nfs-form' },
+ rgw_index: { url: '#rgw/nfs', id: 'cd-nfs-list' },
+ rgw_create: { url: '#rgw/nfs/create', id: 'cd-nfs-form' }
};
export class NFSPageHelper extends PageHelper {
pages = pages;
-
- @PageHelper.restrictTo(pages.create.url)
create(backend: string, squash: string, client: object, pseudo: string, rgwPath?: string) {
this.selectOption('cluster_id', 'testnfs');
- // select a storage backend
- this.selectOption('name', backend);
if (backend === 'CephFS') {
this.selectOption('fs_name', 'myfs');
-
cy.get('#security_label').click({ force: true });
} else {
cy.get('input[data-testid=rgw_path]').type(rgwPath);
cy.get('cd-submit-button').click();
}
- editExport(pseudo: string, editPseudo: string) {
- this.navigateEdit(pseudo);
+ editExport(pseudo: string, editPseudo: string, url: string) {
+ this.navigateEdit(pseudo, true, true, url);
cy.get('input[name=pseudo]').clear().type(editPseudo);
/**
* Navigates to the edit page
*/
- navigateEdit(name: string, select = true, breadcrumb = true) {
+ navigateEdit(name: string, select = true, breadcrumb = true, navigateTo: string = null) {
if (select) {
- this.navigateTo();
+ this.navigateTo(navigateTo);
this.getFirstTableCell(name).click();
}
cy.contains('Creating...').should('not.exist');
{
path: 'cephfs',
canActivate: [FeatureTogglesGuardService],
- data: { breadcrumbs: 'File/File Systems' },
children: [
- { path: '', component: CephfsListComponent },
{
- path: URLVerbs.CREATE,
+ path: 'fs',
+ component: CephfsListComponent,
+ data: { breadcrumbs: 'File/File Systems' }
+ },
+ {
+ path: `fs/${URLVerbs.CREATE}`,
component: CephfsVolumeFormComponent,
data: { breadcrumbs: ActionLabels.CREATE }
},
{
- path: `${URLVerbs.EDIT}/:id`,
+ path: `fs/${URLVerbs.EDIT}/:id`,
component: CephfsVolumeFormComponent,
data: { breadcrumbs: ActionLabels.EDIT }
+ },
+ {
+ path: 'nfs',
+ canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'nfs-ganesha',
+ redirectTo: 'error',
+ section: 'nfs-ganesha',
+ section_info: 'NFS GANESHA',
+ header: 'NFS-Ganesha is not configured'
+ },
+ breadcrumbs: 'File/NFS'
+ },
+ children: [
+ { path: '', component: NfsListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: NfsFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+ component: NfsFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
}
]
},
data: { breadcrumbs: ActionLabels.EDIT }
}
]
- },
- // NFS
- {
- path: 'nfs',
- canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
- data: {
- moduleStatusGuardConfig: {
- uiApiPath: 'nfs-ganesha',
- redirectTo: 'error',
- section: 'nfs-ganesha',
- section_info: 'NFS GANESHA',
- header: 'NFS-Ganesha is not configured'
- },
- breadcrumbs: 'NFS'
- },
- children: [
- { path: '', component: NfsListComponent },
- {
- path: URLVerbs.CREATE,
- component: NfsFormComponent,
- data: { breadcrumbs: ActionLabels.CREATE }
- },
- {
- path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
- component: NfsFormComponent,
- data: { breadcrumbs: ActionLabels.EDIT }
- }
- ]
}
]
},
private route: ActivatedRoute
) {
super();
- this.editing = this.router.url.startsWith(`/cephfs/${URLVerbs.EDIT}`);
+ this.editing = this.router.url.startsWith(`/cephfs/fs/${URLVerbs.EDIT}`);
this.action = this.editing ? this.actionLabels.EDIT : this.actionLabels.CREATE;
this.resource = $localize`File System`;
this.hosts = {
this.form.setErrors({ cdSubmitButton: true });
},
complete: () => {
- this.router.navigate([BASE_URL]);
+ this.router.navigate([`${BASE_URL}/fs`]);
}
});
} else {
self.form.setErrors({ cdSubmitButton: true });
},
complete: () => {
- this.router.navigate([BASE_URL]);
+ this.router.navigate([`${BASE_URL}/fs`]);
}
});
}
import { HealthService } from '~/app/shared/api/health.service';
import { CephfsAuthModalComponent } from '~/app/ceph/cephfs/cephfs-auth-modal/cephfs-auth-modal.component';
-const BASE_URL = 'cephfs';
+const BASE_URL = 'cephfs/fs';
@Component({
selector: 'cd-cephfs-list',
+export enum SUPPORTED_FSAL {
+ CEPH = 'CEPH',
+ RGW = 'RGW'
+}
export interface NfsFSAbstractionLayer {
- value: string;
+ value: SUPPORTED_FSAL;
descr: string;
disabled: boolean;
}
for="cluster_id">
<span class="required"
i18n>Cluster</span>
- <cd-helper>
- <p i18n>This is the ID of an NFS Service.</p>
- </cd-helper>
</label>
<div class="cd-col-form-input">
<select class="form-select"
<option *ngFor="let cluster of allClusters"
[value]="cluster.cluster_id">{{ cluster.cluster_id }}</option>
</select>
+ <cd-help-text>
+ <p i18n>This is the ID of an NFS Service.</p>
+ </cd-help-text>
<span class="invalid-feedback"
*ngIf="nfsForm.showError('cluster_id', formDir, 'required') || allClusters?.length === 0"
i18n>This field is required.
<!-- FSAL -->
<div formGroupName="fsal">
- <!-- Name -->
- <div class="form-group row">
- <label class="cd-col-form-label required"
- for="name"
- i18n>Storage Backend</label>
- <div class="cd-col-form-input">
- <select class="form-select"
- formControlName="name"
- name="name"
- id="name"
- (change)="fsalChangeHandler()">
- <option *ngIf="allFsals === null"
- value=""
- i18n>Loading...</option>
- <option *ngIf="allFsals !== null && allFsals.length === 0"
- value=""
- i18n>-- No data pools available --</option>
- <option *ngIf="allFsals !== null && allFsals.length > 0"
- value=""
- i18n>-- Select the storage backend --</option>
- <option *ngFor="let fsal of allFsals"
- [value]="fsal.value"
- [disabled]="fsal.disabled">{{ fsal.descr }}</option>
- </select>
- <span class="invalid-feedback"
- *ngIf="nfsForm.showError('name', formDir, 'required')"
- i18n>This field is required.</span>
- <span class="invalid-feedback"
- *ngIf="fsalAvailabilityError"
- i18n>{{ fsalAvailabilityError }}</span>
- </div>
- </div>
-
<!-- CephFS Volume -->
<div class="form-group row"
- *ngIf="nfsForm.getValue('name') === 'CEPH'">
+ *ngIf="storageBackend === 'CEPH'">
<label class="cd-col-form-label required"
for="fs_name"
i18n>Volume</label>
<!-- Security Label -->
<div class="form-group row"
- *ngIf="nfsForm.getValue('name') === 'CEPH'">
+ *ngIf="storageBackend === 'CEPH'">
<label class="cd-col-form-label"
[ngClass]="{'required': nfsForm.getValue('security_label')}"
for="security_label"
<!-- Path -->
<div class="form-group row"
- *ngIf="nfsForm.getValue('name') === 'CEPH'">
+ *ngIf="storageBackend === 'CEPH'">
<label class="cd-col-form-label"
for="path">
<span class="required"
i18n>CephFS Path</span>
- <cd-helper>
- <p i18n>A path in a CephFS file system.</p>
- </cd-helper>
</label>
<div class="cd-col-form-input">
<input type="text"
[ngbTypeahead]="pathDataSource"
(selectItem)="pathChangeHandler()"
(blur)="pathChangeHandler()">
+ <cd-help-text>
+ <p i18n>A path in a CephFS file system.</p>
+ </cd-help-text>
<span class="invalid-feedback"
*ngIf="nfsForm.showError('path', formDir, 'required')"
i18n>This field is required.</span>
<!-- Bucket -->
<div class="form-group row"
- *ngIf="nfsForm.getValue('name') === 'RGW'">
+ *ngIf="storageBackend === 'RGW'">
<label class="cd-col-form-label"
for="path">
<span class="required"
for="pseudo">
<span class="required"
i18n>Pseudo</span>
- <cd-helper>
- <p i18n>The position that this <strong>NFS v4</strong> export occupies
- in the <strong>Pseudo FS</strong> (it must be unique).</p>
- <p i18n>By using different Pseudo options, the same Path may be exported multiple times.</p>
- </cd-helper>
</label>
<div class="cd-col-form-input">
<input type="text"
id="pseudo"
formControlName="pseudo"
minlength="2">
+ <cd-help-text>
+ <span i18n>The position this export occupies in the Pseudo FS. It must be unique.</span><br/>
+ <span i18n>By using different Pseudo options, the same Path may be exported multiple times.</span>
+ </cd-help-text>
<span class="invalid-feedback"
*ngIf="nfsForm.showError('pseudo', formDir, 'required')"
i18n>This field is required.</span>
{{ getAccessTypeHelp(nfsForm.getValue('access_type')) }}
</span>
<span class="form-text text-warning"
- *ngIf="nfsForm.getValue('access_type') === 'RW' && nfsForm.getValue('name') === 'RGW'"
+ *ngIf="nfsForm.getValue('access_type') === 'RW' && storageBackend === 'RGW'"
i18n>The Object Gateway NFS backend has a number of
limitations which will seriously affect applications writing to
the share. Please consult the <cd-doc section="rgw-nfs"></cd-doc>
<label class="cd-col-form-label"
for="squash">
<span i18n>Squash</span>
- <ng-container *ngTemplateOutlet="squashHelper"></ng-container>
</label>
<div class="cd-col-form-input">
<select class="form-select"
[value]="squash">{{ squash }}</option>
</select>
+ <cd-help-text>
+ <span *ngIf="nfsForm.getValue('squash') === 'root_squash'"
+ i18n>Maps the root user on the NFS client to an anonymous user/group with limited privileges. This prevents a root client user from having total control over the NFS export.</span>
+
+ <span *ngIf="nfsForm.getValue('squash') === 'root_id_squash'"
+ i18n>Maps the root user on the NFS client to an anonymous user/group with limited privileges, preventing root access but retaining non-root group privileges.</span>
+
+ <span *ngIf="nfsForm.getValue('squash') === 'all_squash'"
+ i18n>Maps all users on the NFS client to an anonymous user/group with limited privileges, ensuring that no user has special privileges on the NFS export.</span>
+
+ <span *ngIf="nfsForm.getValue('squash') === 'no_root_squash'"
+ i18n>Allows the root user on the NFS client to retain full root privileges on the NFS server, which may pose security risks.</span>
+
+ </cd-help-text>
<span class="invalid-feedback"
*ngIf="nfsForm.showError('squash', formDir,'required')"
i18n>This field is required.</span>
<cd-nfs-form-client [form]="nfsForm"
[clients]="clients"
#nfsClients>
- <ng-template #squashHelper>
- <cd-helper>
- <ul class="squash-helper">
- <li>
- <span class="squash-helper-item-value">no_root_squash: </span>
- <span i18n>No user id squashing is performed.</span>
- </li>
- <li>
- <span class="squash-helper-item-value">root_id_squash: </span>
- <span i18n>uid 0 and gid 0 are squashed to the Anonymous_Uid and Anonymous_Gid gid 0 in alt_groups lists is also squashed.</span>
- </li>
- <li>
- <span class="squash-helper-item-value">root_squash: </span>
- <span i18n>uid 0 and gid of any value are squashed to the Anonymous_Uid and Anonymous_Gid alt_groups lists is discarded.</span>
- </li>
- <li>
- <span class="squash-helper-item-value">all_squash: </span>
- <span i18n>All users are squashed.</span>
- </li>
- </ul>
- </cd-helper>
- </ng-template>
</cd-nfs-form-client>
+ <!-- Errors -->
+ <cd-alert-panel type="error"
+ *ngIf="!!storageBackendError">
+ {{storageBackendError}}
+ </cd-alert-panel>
</div>
-
<div class="card-footer">
<cd-form-button-panel (submitActionEvent)="submitAction()"
[form]="nfsForm"
+ [disabled]="!!storageBackendError"
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"
wrappingClass="text-right"></cd-form-button-panel>
</div>
.cd-mb {
margin-bottom: 10px;
}
-
-.squash-helper {
- padding-left: 1rem;
-}
-
-.squash-helper-item-value {
- font-weight: bold;
-}
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ReactiveFormsModule } from '@angular/forms';
-import { ActivatedRoute } from '@angular/router';
+import { ActivatedRoute, Router } from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { NgbTypeaheadModule } from '@ng-bootstrap/ng-bootstrap';
let fixture: ComponentFixture<NfsFormComponent>;
let httpTesting: HttpTestingController;
let activatedRoute: ActivatedRouteStub;
+ let router: Router;
configureTestBed(
{
const matchSquash = (backendSquashValue: string, uiSquashValue: string) => {
component.ngOnInit();
- httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
- httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+ httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
httpTesting.expectOne('api/nfs-ganesha/export/mynfs/1').flush({
fsal: {
name: 'RGW'
component = fixture.componentInstance;
httpTesting = TestBed.inject(HttpTestingController);
activatedRoute = <ActivatedRouteStub>TestBed.inject(ActivatedRoute);
+ router = TestBed.inject(Router);
+
+ Object.defineProperty(router, 'url', {
+ get: jasmine.createSpy('url').and.returnValue('/cephfs/nfs')
+ });
RgwHelper.selectDaemon();
fixture.detectChanges();
- httpTesting.expectOne('ui-api/nfs-ganesha/fsals').flush(['CEPH', 'RGW']);
- httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
httpTesting.expectOne('api/nfs-ganesha/cluster').flush(['mynfs']);
+ httpTesting.expectOne('ui-api/nfs-ganesha/cephfs/filesystems').flush([{ id: 1, name: 'a' }]);
httpTesting.verify();
});
expect(component).toBeTruthy();
});
- it('should process all data', () => {
- expect(component.allFsals).toEqual([
- { descr: 'CephFS', value: 'CEPH', disabled: false },
- { descr: 'Object Gateway', value: 'RGW', disabled: false }
- ]);
- expect(component.allFsNames).toEqual([{ id: 1, name: 'a' }]);
- expect(component.allClusters).toEqual([{ cluster_id: 'mynfs' }]);
- });
-
it('should create the form', () => {
expect(component.nfsForm.value).toEqual({
access_type: 'RW',
-import { ChangeDetectorRef, Component, OnInit, ViewChild } from '@angular/core';
+import { Component, OnInit, ViewChild } from '@angular/core';
import {
AbstractControl,
AsyncValidatorFn,
import { forkJoin, Observable, of } from 'rxjs';
import { catchError, debounceTime, distinctUntilChanged, map, mergeMap } from 'rxjs/operators';
-import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
import { Directory, NfsService } from '~/app/shared/api/nfs.service';
import { RgwBucketService } from '~/app/shared/api/rgw-bucket.service';
import { RgwSiteService } from '~/app/shared/api/rgw-site.service';
import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { NfsFormClientComponent } from '../nfs-form-client/nfs-form-client.component';
+import { getFsalFromRoute, getPathfromFsal } from '../utils';
@Component({
selector: 'cd-nfs-form',
allClusters: { cluster_id: string }[] = null;
icons = Icons;
- allFsals: any[] = [];
allFsNames: any[] = null;
- fsalAvailabilityError: string = null;
+
+ storageBackend: SUPPORTED_FSAL;
+ storageBackendError: string = null;
defaultAccessType = { RGW: 'RO' };
nfsAccessType: any[] = [];
private rgwSiteService: RgwSiteService,
private formBuilder: CdFormBuilder,
private taskWrapper: TaskWrapperService,
- private cdRef: ChangeDetectorRef,
public actionLabels: ActionLabelsI18n
) {
super();
this.permission = this.authStorageService.getPermissions().pool;
this.resource = $localize`NFS export`;
+ this.storageBackend = getFsalFromRoute(this.router.url);
}
ngOnInit() {
this.nfsAccessType = this.nfsService.nfsAccessType;
this.nfsSquash = Object.keys(this.nfsService.nfsSquash);
this.createForm();
- const promises: Observable<any>[] = [
- this.nfsService.listClusters(),
- this.nfsService.fsals(),
- this.nfsService.filesystems()
- ];
+ const promises: Observable<any>[] = [this.nfsService.listClusters()];
- if (this.router.url.startsWith('/nfs/edit')) {
+ if (this.storageBackend === 'RGW') {
+ promises.push(this.rgwSiteService.get('realms'));
+ } else {
+ promises.push(this.nfsService.filesystems());
+ }
+
+ if (this.router.url.startsWith(`/${getPathfromFsal(this.storageBackend)}/nfs/edit`)) {
this.isEdit = true;
}
this.cluster_id = decodeURIComponent(params.cluster_id);
this.export_id = decodeURIComponent(params.export_id);
promises.push(this.nfsService.get(this.cluster_id, this.export_id));
-
this.getData(promises);
});
this.nfsForm.get('cluster_id').disable();
forkJoin(promises).subscribe((data: any[]) => {
this.resolveClusters(data[0]);
this.resolveFsals(data[1]);
- this.resolveFilesystems(data[2]);
- if (data[3]) {
- this.resolveModel(data[3]);
+ if (data[2]) {
+ this.resolveModel(data[2]);
}
-
this.loadingReady();
});
}
validators: [Validators.required]
}),
fsal: new CdFormGroup({
- name: new UntypedFormControl('', {
+ name: new UntypedFormControl(this.storageBackend, {
validators: [Validators.required]
}),
fs_name: new UntypedFormControl('', {
]
})
}),
- path: new UntypedFormControl('/'),
+ path: new UntypedFormControl('/', {
+ validators: [Validators.required]
+ }),
protocolNfsv3: new UntypedFormControl(true, {
validators: [
CdValidators.requiredIf({ protocolNfsv4: false }, (value: boolean) => {
}
resolveFsals(res: string[]) {
- res.forEach((fsal) => {
- const fsalItem = this.nfsService.nfsFsal.find((currentFsalItem) => {
- return fsal === currentFsalItem.value;
- });
-
- if (_.isObjectLike(fsalItem)) {
- this.allFsals.push(fsalItem);
- }
- });
- if (!this.isEdit && this.allFsals.length > 0) {
+ if (this.storageBackend === 'RGW') {
+ this.setPathValidation();
+ this.resolveRealms(res);
+ } else {
+ this.resolveFilesystems(res);
+ }
+ if (!this.isEdit && this.storageBackend === SUPPORTED_FSAL.RGW) {
this.nfsForm.patchValue({
- fsal: {
- name: this.allFsals[0].value
- }
+ path: '',
+ access_type: this.defaultAccessType[SUPPORTED_FSAL.RGW]
});
}
}
}
}
- fsalChangeHandler() {
- this.setPathValidation();
- const fsalValue = this.nfsForm.getValue('name');
- const checkAvailability =
- fsalValue === 'RGW'
- ? this.rgwSiteService.get('realms').pipe(
- mergeMap((realms: string[]) =>
- realms.length === 0
- ? of(true)
- : this.rgwSiteService.isDefaultRealm().pipe(
- mergeMap((isDefaultRealm) => {
- if (!isDefaultRealm) {
- throw new Error('Selected realm is not the default.');
- }
- return of(true);
- })
- )
- )
- )
- : this.nfsService.filesystems();
-
- checkAvailability.subscribe({
- next: () => {
- this.setFsalAvailability(fsalValue, true);
- if (!this.isEdit) {
- this.nfsForm.patchValue({
- path: fsalValue === 'RGW' ? '' : '/',
- pseudo: this.generatePseudo(),
- access_type: this.updateAccessType()
- });
- }
-
- this.cdRef.detectChanges();
- },
- error: (error) => {
- this.setFsalAvailability(fsalValue, false, error);
- this.nfsForm.get('name').setValue('');
- }
- });
- }
-
- private setFsalAvailability(fsalValue: string, available: boolean, errorMessage: string = '') {
- this.allFsals = this.allFsals.map((fsalItem: NfsFSAbstractionLayer) => {
- if (fsalItem.value === fsalValue) {
- fsalItem.disabled = !available;
-
- this.fsalAvailabilityError = fsalItem.disabled
- ? $localize`${fsalItem.descr} backend is not available. ${errorMessage}`
- : null;
- }
- return fsalItem;
- });
+ resolveRealms(realms: string[]) {
+ if (realms.length !== 0) {
+ this.rgwSiteService
+ .isDefaultRealm()
+ .pipe(
+ mergeMap((isDefaultRealm) => {
+ if (!isDefaultRealm) {
+ throw new Error('Selected realm is not the default.');
+ }
+ return of(true);
+ })
+ )
+ .subscribe({
+ error: (error) => {
+ const fsalDescr = this.nfsService.nfsFsal.find((f) => f.value === this.storageBackend)
+ .descr;
+ this.storageBackendError = $localize`${fsalDescr} backend is not available. ${error}`;
+ }
+ });
+ }
}
accessTypeChangeHandler() {
setPathValidation() {
const path = this.nfsForm.get('path');
- path.setValidators([Validators.required]);
- if (this.nfsForm.getValue('name') === 'RGW') {
+ if (this.storageBackend === SUPPORTED_FSAL.RGW) {
path.setAsyncValidators([CdValidators.bucketExistence(true, this.rgwBucketService)]);
} else {
path.setAsyncValidators([this.pathExistence(true)]);
let newPseudo = this.nfsForm.getValue('pseudo');
if (this.nfsForm.get('pseudo') && !this.nfsForm.get('pseudo').dirty) {
newPseudo = undefined;
- if (this.nfsForm.getValue('fsal') === 'CEPH') {
+ if (this.storageBackend === 'CEPH') {
newPseudo = '/cephfs';
if (_.isString(this.nfsForm.getValue('path'))) {
newPseudo += this.nfsForm.getValue('path');
return newPseudo;
}
- private updateAccessType() {
- const name = this.nfsForm.getValue('name');
- let accessType = this.defaultAccessType[name];
-
- if (!accessType) {
- accessType = 'RW';
- }
-
- return accessType;
- }
-
submitAction() {
let action: Observable<any>;
const requestModel = this.buildRequest();
action.subscribe({
error: (errorResponse: CdHttpErrorResponse) => this.setFormErrors(errorResponse),
- complete: () => this.router.navigate(['/nfs'])
+ complete: () => this.router.navigate([`/${getPathfromFsal(this.storageBackend)}/nfs`])
});
}
import { configureTestBed, expectItemTasks, PermissionHelper } from '~/testing/unit-test-helper';
import { NfsDetailsComponent } from '../nfs-details/nfs-details.component';
import { NfsListComponent } from './nfs-list.component';
+import { SUPPORTED_FSAL } from '../models/nfs.fsal';
describe('NfsListComponent', () => {
let component: NfsListComponent;
beforeEach(() => {
fixture = TestBed.createComponent(NfsListComponent);
component = fixture.componentInstance;
+ component.fsal = SUPPORTED_FSAL.CEPH;
summaryService = TestBed.inject(SummaryService);
nfsService = TestBed.inject(NfsService);
httpTesting = TestBed.inject(HttpTestingController);
const model = {
export_id: export_id,
path: 'path_' + export_id,
- fsal: 'fsal_' + export_id,
+ fsal: {
+ name: 'CEPH'
+ },
cluster_id: 'cluster_' + export_id
};
exports.push(model);
case 'nfs/create':
task.metadata = {
path: 'path_' + export_id,
- fsal: 'fsal_' + export_id,
+ fsal: {
+ name: 'CEPH'
+ },
cluster_id: 'cluster_' + export_id
};
break;
import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Router } from '@angular/router';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import _ from 'lodash';
import { ModalService } from '~/app/shared/services/modal.service';
import { TaskListService } from '~/app/shared/services/task-list.service';
import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
+import { getFsalFromRoute, getPathfromFsal } from '../utils';
+import { SUPPORTED_FSAL } from '../models/nfs.fsal';
@Component({
selector: 'cd-nfs-list',
exports: any[];
tableActions: CdTableAction[];
isDefaultCluster = false;
+ fsal: SUPPORTED_FSAL;
modalRef: NgbModalRef;
private nfsService: NfsService,
private taskListService: TaskListService,
private taskWrapper: TaskWrapperService,
+ private router: Router,
public actionLabels: ActionLabelsI18n
) {
super();
this.permission = this.authStorageService.getPermissions().nfs;
+ this.fsal = getFsalFromRoute(this.router.url);
+ const prefix = getPathfromFsal(this.fsal);
const getNfsUri = () =>
this.selection.first() &&
`${encodeURI(this.selection.first().cluster_id)}/${encodeURI(
const createAction: CdTableAction = {
permission: 'create',
icon: Icons.add,
- routerLink: () => '/nfs/create',
+ routerLink: () => `/${prefix}/nfs/create`,
canBePrimary: (selection: CdTableSelection) => !selection.hasSingleSelection,
name: this.actionLabels.CREATE
};
const editAction: CdTableAction = {
permission: 'update',
icon: Icons.edit,
- routerLink: () => `/nfs/edit/${getNfsUri()}`,
+ routerLink: () => `/${prefix}/nfs/edit/${getNfsUri()}`,
name: this.actionLabels.EDIT
};
ngOnInit() {
this.columns = [
{
- name: $localize`Path`,
+ name: this.fsal === SUPPORTED_FSAL.CEPH ? $localize`Path` : $localize`Bucket`,
prop: 'path',
flexGrow: 2,
cellTransformation: CellTemplate.executing
prepareResponse(resp: any): any[] {
let result: any[] = [];
- resp.forEach((nfs: any) => {
+ const filteredresp = resp.filter((nfs: any) => nfs.fsal?.name === this.fsal);
+ filteredresp.forEach((nfs: any) => {
nfs.id = `${nfs.cluster_id}:${nfs.export_id}`;
nfs.state = 'LOADING';
result = result.concat(nfs);
});
-
return result;
}
NgbTypeaheadModule,
NgbTooltipModule
],
+ exports: [NfsListComponent, NfsFormComponent, NfsDetailsComponent],
declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
})
export class NfsModule {}
--- /dev/null
+import { SUPPORTED_FSAL } from './models/nfs.fsal';
+
+export const getFsalFromRoute = (url: string): SUPPORTED_FSAL =>
+ url.startsWith('/rgw/nfs') ? SUPPORTED_FSAL.RGW : SUPPORTED_FSAL.CEPH;
+
+export const getPathfromFsal = (fsal: SUPPORTED_FSAL): string =>
+ fsal === SUPPORTED_FSAL.CEPH ? 'cephfs' : 'rgw';
import { ActionLabels, URLVerbs } from '~/app/shared/constants/app.constants';
import { CRUDTableComponent } from '~/app/shared/datatable/crud-table/crud-table.component';
+import { FeatureTogglesGuardService } from '~/app/shared/services/feature-toggles-guard.service';
+import { ModuleStatusGuardService } from '~/app/shared/services/module-status-guard.service';
import { SharedModule } from '~/app/shared/shared.module';
import { PerformanceCounterModule } from '../performance-counter/performance-counter.module';
import { RgwSyncMetadataInfoComponent } from './rgw-sync-metadata-info/rgw-sync-metadata-info.component';
import { RgwSyncDataInfoComponent } from './rgw-sync-data-info/rgw-sync-data-info.component';
import { BucketTagModalComponent } from './bucket-tag-modal/bucket-tag-modal.component';
+import { NfsListComponent } from '../nfs/nfs-list/nfs-list.component';
+import { NfsFormComponent } from '../nfs/nfs-form/nfs-form.component';
@NgModule({
imports: [
path: 'multisite',
data: { breadcrumbs: 'Multi-site' },
children: [{ path: '', component: RgwMultisiteDetailsComponent }]
+ },
+ {
+ path: 'nfs',
+ canActivateChild: [FeatureTogglesGuardService, ModuleStatusGuardService],
+ data: {
+ moduleStatusGuardConfig: {
+ uiApiPath: 'nfs-ganesha',
+ redirectTo: 'error',
+ section: 'nfs-ganesha',
+ section_info: 'NFS GANESHA',
+ header: 'NFS-Ganesha is not configured'
+ },
+ breadcrumbs: 'NFS'
+ },
+ children: [
+ { path: '', component: NfsListComponent },
+ {
+ path: URLVerbs.CREATE,
+ component: NfsFormComponent,
+ data: { breadcrumbs: ActionLabels.CREATE }
+ },
+ {
+ path: `${URLVerbs.EDIT}/:cluster_id/:export_id`,
+ component: NfsFormComponent,
+ data: { breadcrumbs: ActionLabels.EDIT }
+ }
+ ]
}
];
import { Observable, throwError } from 'rxjs';
-import { NfsFSAbstractionLayer } from '~/app/ceph/nfs/models/nfs.fsal';
+import { NfsFSAbstractionLayer, SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
import { ApiClient } from '~/app/shared/api/api-client';
export interface Directory {
nfsFsal: NfsFSAbstractionLayer[] = [
{
- value: 'CEPH',
+ value: SUPPORTED_FSAL.CEPH,
descr: $localize`CephFS`,
disabled: false
},
{
- value: 'RGW',
+ value: SUPPORTED_FSAL.RGW,
descr: $localize`Object Gateway`,
disabled: false
}