from ..services.cephfs import CephFS
from ..services.exception import DashboardException, handle_cephfs_error, \
serialize_dashboard_exception
+from ..tools import str_to_bool
from . import APIDoc, APIRouter, BaseController, Endpoint, EndpointDoc, \
ReadPermission, RESTController, Task, UIRouter
from ._version import APIVersion
class NFSGaneshaCluster(RESTController):
@ReadPermission
@RESTController.MethodMap(version=APIVersion.EXPERIMENTAL)
- def list(self):
+ def list(self, info: Optional[bool] = False):
+ if str_to_bool(info):
+ return [
+ {"name": key, **value} for key, value in mgr.remote('nfs', 'cluster_info').items()
+ ]
return mgr.remote('nfs', 'cluster_ls')
export['fsal'] = schema_fsal_info
return export
- @EndpointDoc("List all NFS-Ganesha exports",
+ @EndpointDoc("List all or cluster specific NFS-Ganesha exports ",
responses={200: [EXPORT_SCHEMA]})
- def list(self) -> List[Dict[str, Any]]:
+ def list(self, cluster_id=None) -> List[Dict[str, Any]]:
exports = []
- for export in mgr.remote('nfs', 'export_ls'):
+ for export in mgr.remote('nfs', 'export_ls', cluster_id, True):
exports.append(self._get_schema_export(export))
return exports
import { TelemetryComponent } from './ceph/cluster/telemetry/telemetry.component';
import { DashboardComponent } from './ceph/dashboard/dashboard/dashboard.component';
import { NfsFormComponent } from './ceph/nfs/nfs-form/nfs-form.component';
-import { NfsListComponent } from './ceph/nfs/nfs-list/nfs-list.component';
import { PerformanceCounterComponent } from './ceph/performance-counter/performance-counter/performance-counter.component';
import { LoginPasswordFormComponent } from './core/auth/login-password-form/login-password-form.component';
import { LoginComponent } from './core/auth/login/login.component';
import { SmbShareFormComponent } from './ceph/smb/smb-share-form/smb-share-form.component';
import { SmbJoinAuthFormComponent } from './ceph/smb/smb-join-auth-form/smb-join-auth-form.component';
import { SmbUsersgroupsFormComponent } from './ceph/smb/smb-usersgroups-form/smb-usersgroups-form.component';
+import { NfsClusterComponent } from './ceph/nfs/nfs-cluster/nfs-cluster.component';
@Injectable()
export class PerformanceCounterBreadcrumbsResolver extends BreadcrumbsResolver {
breadcrumbs: 'File/NFS'
},
children: [
- { path: '', component: NfsListComponent },
+ { path: '', component: NfsClusterComponent },
{
path: `${URLVerbs.CREATE}/:fs_name/:subvolume_group`,
component: NfsFormComponent,
--- /dev/null
+export interface NFSBackend {
+ hostname: string;
+ ip: string;
+ port: number;
+}
+
+export interface NFSCluster {
+ name: string;
+ virtual_ip: number;
+ port: number;
+ backend: NFSBackend[];
+}
--- /dev/null
+ <ng-container *ngIf="!!selection">
+ <legend
+ i18n
+ >
+ {{title | titlecase}}
+ <cd-help-text>
+ Lists exports for a cluster
+ </cd-help-text>
+ </legend>
+ <cd-nfs-list [clusterId]="selection.name"></cd-nfs-list>
+ </ng-container>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { NfsClusterDetailsComponent } from './nfs-cluster-details.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SharedModule } from '~/app/shared/shared.module';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+describe('NfsClusterDetailsComponent', () => {
+ let component: NfsClusterDetailsComponent;
+ let fixture: ComponentFixture<NfsClusterDetailsComponent>;
+
+ configureTestBed({
+ declarations: [NfsClusterDetailsComponent],
+ imports: [HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NfsClusterDetailsComponent);
+ component = fixture.componentInstance;
+ });
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input } from '@angular/core';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+
+@Component({
+ selector: 'cd-nfs-cluster-details',
+ templateUrl: './nfs-cluster-details.component.html',
+ styleUrls: ['./nfs-cluster-details.component.scss']
+})
+export class NfsClusterDetailsComponent {
+ title = $localize`Export`;
+ @Input()
+ selection: CdTableSelection;
+}
--- /dev/null
+ <ng-container *ngIf="orchStatus?.available && clusters$ | async as clusters">
+ <cd-table [autoReload]="true"
+ [data]="clusters"
+ columnMode="flex"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="true"
+ (setExpandedRow)="setExpandedRow($event)"
+ (fetchData)="loadData()"
+ (updateSelection)="updateSelection($event)">
+ <cd-table-actions class="table-actions"
+ [selection]="selection"
+ [permission]="permission"
+ [tableActions]="tableActions"></cd-table-actions>
+ <cd-nfs-cluster-details *cdTableDetail
+ [selection]="expandedRow"></cd-nfs-cluster-details>
+ </cd-table>
+ </ng-container>
+
+ <ng-template
+ #virtualIpTpl
+ let-row="data.row"
+ >
+ <span *ngIf="row.virtual_ip || row.ports">
+ <cds-tag size="md">
+ {{ row.virtual_ip }}:{{row.ports}}
+ </cds-tag>
+ </span>
+ </ng-template>
+
+ <ng-template
+ #ipAddrTpl
+ let-backends="data.value"
+ >
+ <span *ngFor="let backend of backends">
+ <span *ngIf="backend.ip || backend.port">
+ <cds-tag size="md">
+ {{ backend.ip }}:{{backend.port}}
+ </cds-tag>
+ </span>
+ </span>
+ </ng-template>
+
+ <ng-template #hostnameTpl
+ let-backends="data.value">
+ <span *ngFor="let backend of backends">
+ <span *ngIf="backend.hostname">
+ <cds-tag size="md">{{backend.hostname }}</cds-tag>
+ </span>
+ </span>
+ </ng-template>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { SharedModule } from '~/app/shared/shared.module';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { NfsClusterComponent } from './nfs-cluster.component';
+
+describe('NfsClusterComponent', () => {
+ let component: NfsClusterComponent;
+ let fixture: ComponentFixture<NfsClusterComponent>;
+
+ configureTestBed({
+ declarations: [NfsClusterComponent],
+ imports: [HttpClientTestingModule, SharedModule]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(NfsClusterComponent);
+ component = fixture.componentInstance;
+ });
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, NgZone, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { NfsService } from '~/app/shared/api/nfs.service';
+import { ListWithDetails } from '~/app/shared/classes/list-with-details.class';
+import { ActionLabelsI18n } from '~/app/shared/constants/app.constants';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permission } from '~/app/shared/models/permissions';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { URLBuilderService } from '~/app/shared/services/url-builder.service';
+import { NFSCluster } from '../models/nfs-cluster-config';
+import { OrchestratorStatus } from '~/app/shared/models/orchestrator.interface';
+import { OrchestratorService } from '~/app/shared/api/orchestrator.service';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { switchMap } from 'rxjs/operators';
+const BASE_URL = 'cephfs/nfs';
+@Component({
+ selector: 'cd-nfs-cluster',
+ templateUrl: './nfs-cluster.component.html',
+ styleUrls: ['./nfs-cluster.component.scss'],
+ providers: [{ provide: URLBuilderService, useValue: new URLBuilderService(BASE_URL) }]
+})
+export class NfsClusterComponent extends ListWithDetails implements OnInit {
+ @ViewChild('hostnameTpl', { static: true })
+ hostnameTpl: TemplateRef<any>;
+
+ @ViewChild('ipAddrTpl', { static: true })
+ ipAddrTpl: TemplateRef<any>;
+
+ @ViewChild('virtualIpTpl', { static: true })
+ virtualIpTpl: TemplateRef<any>;
+
+ columns: CdTableColumn[] = [];
+ selection: CdTableSelection = new CdTableSelection();
+ tableActions: CdTableAction[] = [];
+ permission: Permission;
+ orchStatus: OrchestratorStatus;
+ clusters$: Observable<NFSCluster[]>;
+ subject = new BehaviorSubject<NFSCluster[]>([]);
+
+ constructor(
+ public actionLabels: ActionLabelsI18n,
+ protected ngZone: NgZone,
+ private authStorageService: AuthStorageService,
+ private nfsService: NfsService,
+ private orchService: OrchestratorService
+ ) {
+ super();
+ }
+
+ ngOnInit(): void {
+ this.orchService.status().subscribe((status: OrchestratorStatus) => {
+ this.orchStatus = status;
+ });
+ this.permission = this.authStorageService.getPermissions().nfs;
+ this.clusters$ = this.subject.pipe(switchMap(() => this.nfsService.nfsClusterList()));
+ this.columns = [
+ {
+ name: $localize`Name`,
+ prop: 'name',
+ flexGrow: 1
+ },
+ {
+ name: $localize`Hostnames`,
+ prop: 'backend',
+ flexGrow: 2,
+ cellTemplate: this.hostnameTpl
+ },
+ {
+ name: $localize`IP Address`,
+ prop: 'backend',
+ flexGrow: 2,
+ cellTemplate: this.ipAddrTpl
+ },
+ {
+ name: $localize`Virtual IP Address`,
+ prop: 'virtual_ip',
+ flexGrow: 1,
+ cellTemplate: this.virtualIpTpl
+ }
+ ];
+ }
+
+ loadData() {
+ this.subject.next([]);
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+}
identifier="id"
forceIdentifier="true"
selectionType="single"
- [hasDetails]="true"
+ [hasDetails]="false"
(setExpandedRow)="setExpandedRow($event)"
(updateSelection)="updateSelection($event)">
<div class="table-actions">
i18n>Object Gateway</ng-container>
</ng-template>
+<ng-template #protocolTpl
+ let-protocols="data.value">
+ <span *ngFor="let protocol of protocols">
+ <cds-tag size="md">NFSv{{protocol}}</cds-tag>
+ </span>
+</ng-template>
+
+<ng-template #transportTpl
+ let-transports="data.value">
+ <span *ngFor="let transport of transports">
+ <cds-tag size="md">{{transport}}</cds-tag>
+ </span>
+</ng-template>
+
<ng-template #pathTmpl
let-value="data.value">
<span *ngIf="value === ''"
httpTesting.verify();
});
- it('should load exports on init', () => {
- refresh(new Summary());
- httpTesting.expectOne('api/nfs-ganesha/export');
- expect(nfsService.list).toHaveBeenCalled();
- });
-
it('should not load images on init because no data', () => {
refresh(undefined);
expect(nfsService.list).not.toHaveBeenCalled();
-import { Component, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
+import { Component, Input, OnDestroy, OnInit, TemplateRef, ViewChild } from '@angular/core';
import { Router } from '@angular/router';
import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
@ViewChild('table', { static: true })
table: TableComponent;
+ @ViewChild('protocolTpl', { static: true })
+ protocolTpl: TemplateRef<any>;
+
+ @ViewChild('transportTpl', { static: true })
+ transportTpl: TemplateRef<any>;
+
+ @Input() clusterId: string;
+ modalRef: NgbModalRef;
+
columns: CdTableColumn[];
permission: Permission;
selection = new CdTableSelection();
isDefaultCluster = false;
fsal: SUPPORTED_FSAL;
- modalRef: NgbModalRef;
-
builders = {
'nfs/create': (metadata: any) => {
return {
name: this.fsal === SUPPORTED_FSAL.CEPH ? $localize`Path` : $localize`Bucket`,
prop: 'path',
flexGrow: 2,
- cellTemplate: this.pathTmpl
+ cellTemplate: this.pathTmpl,
+ cellTransformation: CellTemplate.path
},
{
name: $localize`Pseudo`,
name: $localize`Access Type`,
prop: 'access_type',
flexGrow: 2
+ },
+ {
+ name: $localize`NFS Protocol`,
+ prop: 'protocols',
+ flexGrow: 2,
+ cellTemplate: this.protocolTpl
+ },
+ {
+ name: $localize`Transports`,
+ prop: 'transports',
+ flexGrow: 2,
+ cellTemplate: this.transportTpl
}
];
this.taskListService.init(
- () => this.nfsService.list(),
+ () => this.nfsService.list(this.clusterId),
(resp) => this.prepareResponse(resp),
(exports) => (this.exports = exports),
() => this.onFetchError(),
IconService,
InputModule,
RadioModule,
- SelectModule
+ SelectModule,
+ TabsModule,
+ TagModule
} from 'carbon-components-angular';
import Close from '@carbon/icons/es/close/32';
+import { NfsClusterComponent } from './nfs-cluster/nfs-cluster.component';
+import { ClusterModule } from '../cluster/cluster.module';
+import { NfsClusterDetailsComponent } from './nfs-cluster-details/nfs-cluster-details.component';
@NgModule({
imports: [
NgbTypeaheadModule,
NgbTooltipModule,
GridModule,
+ TagModule,
SelectModule,
InputModule,
RadioModule,
CheckboxModule,
ButtonModule,
- IconModule
+ IconModule,
+ TabsModule,
+ ClusterModule
],
- exports: [NfsListComponent, NfsFormComponent, NfsDetailsComponent],
- declarations: [NfsListComponent, NfsDetailsComponent, NfsFormComponent, NfsFormClientComponent]
+ exports: [NfsListComponent, NfsFormComponent, NfsDetailsComponent, NfsClusterComponent],
+ declarations: [
+ NfsListComponent,
+ NfsDetailsComponent,
+ NfsFormComponent,
+ NfsFormClientComponent,
+ NfsClusterComponent,
+ NfsClusterDetailsComponent
+ ]
})
export class NfsModule {
constructor(private iconService: IconService) {
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';
import { RgwMultisiteSyncPolicyComponent } from './rgw-multisite-sync-policy/rgw-multisite-sync-policy.component';
import { RgwMultisiteSyncPolicyFormComponent } from './rgw-multisite-sync-policy-form/rgw-multisite-sync-policy-form.component';
import { RgwBucketLifecycleListComponent } from './rgw-bucket-lifecycle-list/rgw-bucket-lifecycle-list.component';
import { RgwRateLimitComponent } from './rgw-rate-limit/rgw-rate-limit.component';
import { RgwRateLimitDetailsComponent } from './rgw-rate-limit-details/rgw-rate-limit-details.component';
+import { NfsClusterComponent } from '../nfs/nfs-cluster/nfs-cluster.component';
@NgModule({
imports: [
breadcrumbs: 'NFS'
},
children: [
- { path: '', component: NfsListComponent },
+ { path: '', component: NfsClusterComponent },
{
path: URLVerbs.CREATE,
component: NfsFormComponent,
});
it('should call list', () => {
- service.list().subscribe();
- const req = httpTesting.expectOne('api/nfs-ganesha/export');
+ let cluster_id = 'test';
+ service.list(cluster_id).subscribe();
+ const req = httpTesting.expectOne('api/nfs-ganesha/export?cluster_id=test');
expect(req.request.method).toBe('GET');
});
import { Injectable } from '@angular/core';
import { Observable, throwError } from 'rxjs';
+import { NFSCluster } from '~/app/ceph/nfs/models/nfs-cluster-config';
import { NfsFSAbstractionLayer, SUPPORTED_FSAL } from '~/app/ceph/nfs/models/nfs.fsal';
import { ApiClient } from '~/app/shared/api/api-client';
disabled: false
}
];
-
nfsSquash = {
no_root_squash: ['no_root_squash', 'noidsquash', 'none'],
root_id_squash: ['root_id_squash', 'rootidsquash', 'rootid'],
super();
}
- list() {
- return this.http.get(`${this.apiPath}/export`);
+ list(clusterId?: string) {
+ return this.http.get(`${this.apiPath}/export`, {
+ params: { cluster_id: clusterId }
+ });
}
get(clusterId: string, exportId: string) {
filesystems() {
return this.http.get(`${this.uiApiPath}/cephfs/filesystems`);
}
+
+ nfsClusterList(): Observable<NFSCluster[]> {
+ return this.http.get<NFSCluster[]>(`${this.apiPath}/cluster`, {
+ headers: { Accept: this.getVersionHeaderValue(0, 1) },
+ params: { info: true }
+ });
+ }
}
- Multi-cluster
/api/nfs-ganesha/cluster:
get:
- parameters: []
+ parameters:
+ - default: false
+ in: query
+ name: info
+ schema:
+ type: boolean
responses:
'200':
content:
- NFS-Ganesha
/api/nfs-ganesha/export:
get:
- parameters: []
+ parameters:
+ - allowEmptyValue: true
+ in: query
+ name: cluster_id
+ schema:
+ type: string
responses:
'200':
content:
trace.
security:
- jwt: []
- summary: List all NFS-Ganesha exports
+ summary: 'List all or cluster specific NFS-Ganesha exports '
tags:
- NFS-Ganesha
post:
def fetch_nfs_export_obj(self) -> ExportMgr:
return self.export_mgr
- def export_ls(self) -> List[Dict[Any, Any]]:
- return self.export_mgr.list_all_exports()
+ def export_ls(self, cluster_id: Optional[str] = None, detailed: bool = False) -> List[Dict[Any, Any]]:
+ if not (cluster_id):
+ return self.export_mgr.list_all_exports()
+ return self.export_mgr.list_exports(cluster_id, detailed)
def export_get(self, cluster_id: str, export_id: int) -> Optional[Dict[str, Any]]:
return self.export_mgr.get_export_by_id(cluster_id, export_id)
def cluster_ls(self) -> List[str]:
return available_clusters(self)
+
+ def cluster_info(self, cluster_id: Optional[str] = None) -> Dict[str, Any]:
+ return self.nfs.show_nfs_cluster_info(cluster_id=cluster_id)
+
+ def fetch_nfs_cluster_obj(self) -> NFSCluster:
+ return self.nfs