import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
import { FinishedTask } from '~/app/shared/models/finished-task';
import { NotificationService } from '~/app/shared/services/notification.service';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component';
+import { map, switchMap } from 'rxjs/operators';
+import { HealthService } from '~/app/shared/api/health.service';
const BASE_URL = 'cephfs';
permissions: Permissions;
icons = Icons;
monAllowPoolDelete = false;
+ modalRef!: NgbModalRef;
constructor(
private authStorageService: AuthStorageService,
private configurationService: ConfigurationService,
private modalService: ModalService,
private taskWrapper: TaskWrapperService,
- public notificationService: NotificationService
+ public notificationService: NotificationService,
+ private healthService: HealthService
) {
super();
this.permissions = this.authStorageService.getPermissions();
click: () =>
this.router.navigate([this.urlBuilder.getEdit(this.selection.first().mdsmap.fs_name)])
},
+ {
+ name: this.actionLabels.ATTACH,
+ permission: 'read',
+ icon: Icons.bars,
+ disable: () => !this.selection?.hasSelection,
+ click: () => this.showAttachInfo()
+ },
{
permission: 'delete',
icon: Icons.destroy,
this.selection = selection;
}
+ showAttachInfo() {
+ const selectedFileSystem = this.selection?.selected?.[0];
+
+ this.cephfsService
+ .getFsRootDirectory(selectedFileSystem.id)
+ .pipe(
+ switchMap((fsData) =>
+ this.healthService.getClusterFsid().pipe(map((data) => ({ clusterId: data, fs: fsData })))
+ )
+ )
+ .subscribe({
+ next: (val) => {
+ this.modalRef = this.modalService.show(CephfsMountDetailsComponent, {
+ onSubmit: () => this.modalRef.close(),
+ mountData: {
+ fsId: val.clusterId,
+ fsName: selectedFileSystem?.mdsmap?.fs_name,
+ rootPath: val.fs['path']
+ }
+ });
+ }
+ });
+ }
+
removeVolumeModal() {
const volName = this.selection.first().mdsmap['fs_name'];
this.modalService.show(CriticalConfirmationModalComponent, {
--- /dev/null
+<cd-modal (hide)="cancel()">
+ <ng-container class="modal-title">
+ <span i18n>Attach commands</span>
+ </ng-container>
+ <ng-container class="modal-content">
+ <div class="modal-body">
+ <h5 class="fw-bold"
+ i18n>
+ Using Mount command
+ </h5>
+ <cd-code-block textWrap="true"
+ [codes]="[mount]"></cd-code-block>
+
+ <h5 class="fw-bold"
+ i18n>
+ Using FUSE command
+ </h5>
+ <cd-code-block textWrap="true"
+ [codes]="[fuse]"></cd-code-block>
+
+ <h5 class="fw-bold"
+ i18n>
+ Using NFS Command
+ </h5>
+ <cd-code-block textWrap="true"
+ [codes]="[nfs]"></cd-code-block>
+ </div>
+ <div class="modal-footer">
+ <cd-submit-button (submitAction)="cancel()"
+ i18n>
+ Close
+ </cd-submit-button>
+ </div>
+ </ng-container>
+</cd-modal>
+
+
+
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsMountDetailsComponent } from './cephfs-mount-details.component';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+import { SharedModule } from '~/app/shared/shared.module';
+import { ToastrModule } from 'ngx-toastr';
+import { RouterTestingModule } from '@angular/router/testing';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CephfsSnapshotscheduleListComponent', () => {
+ let component: CephfsMountDetailsComponent;
+ let fixture: ComponentFixture<CephfsMountDetailsComponent>;
+
+ configureTestBed({
+ declarations: [CephfsMountDetailsComponent],
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsMountDetailsComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, OnDestroy, OnInit, ViewChild } from '@angular/core';
+import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
+
+@Component({
+ selector: 'cd-cephfs-mount-details',
+ templateUrl: './cephfs-mount-details.component.html',
+ styleUrls: ['./cephfs-mount-details.component.scss']
+})
+export class CephfsMountDetailsComponent implements OnInit, OnDestroy {
+ @ViewChild('mountDetailsTpl', { static: true })
+ mountDetailsTpl: any;
+ onCancel?: Function;
+ private canceled = false;
+ private MOUNT_DIRECTORY = '<MOUNT_DIRECTORY>';
+ mountData!: Record<string, any>;
+ constructor(public activeModal: NgbActiveModal) {}
+ mount!: string;
+ fuse!: string;
+ nfs!: string;
+
+ ngOnInit(): void {
+ this.mount = `sudo <CLIENT_USER>@${this.mountData?.fsId}.${this.mountData?.fsName}=${this.mountData?.rootPath} ${this.MOUNT_DIRECTORY}`;
+ this.fuse = `sudo ceph-fuse ${this.MOUNT_DIRECTORY} -r ${this.mountData?.rootPath} --client_mds_namespace=${this.mountData?.fsName}`;
+ this.nfs = `sudo mount -t nfs -o port=<PORT> <IP of active_mds daemon>:${this.mountData?.rootPath} ${this.MOUNT_DIRECTORY}`;
+ }
+
+ ngOnDestroy(): void {
+ if (this.onCancel && this.canceled) {
+ this.onCancel();
+ }
+ }
+
+ cancel() {
+ this.canceled = true;
+ this.activeModal.close();
+ }
+}
class="fw-bold"
[ngbTooltip]="fullpathTpl"
triggers="click:blur">
- {{ row.path?.split?.("@")?.[0] | path }}
+ {{ row.pathForSelection?.split?.("@")?.[0] | path }}
</span>
<span
*ngIf="row.active; else inactiveStatusTpl">
<i
[ngClass]="[icons.success, icons.large]"
- ngbTooltip="{{ row.path?.split?.('@')?.[0] }} is active"
+ ngbTooltip="{{ row.pathForSelection?.split?.('@')?.[0] }} is active"
class="text-success"
></i>
</span>
<i
[ngClass]="[icons.warning, icons.large]"
class="text-warning"
- ngbTooltip="{{ row.path?.split?.('@')?.[0] }} has been deactivated"
+ ngbTooltip="{{ row.pathForSelection?.split?.('@')?.[0] }} has been deactivated"
></i>
</ng-template>
- <ng-template #fullpathTpl>
+ <ng-template #fullpathForSelectionTpl>
<span
data-toggle="tooltip"
- [title]="row.path"
+ [title]="row.pathForSelection"
class="font-monospace"
- >{{ row.path?.split?.("@")?.[0] }}
+ >{{ row.pathForSelection?.split?.("@")?.[0] }}
<cd-copy-2-clipboard-button
- *ngIf="row.path"
- [source]="row.path?.split?.('@')?.[0]"
+ *ngIf="row.pathForSelection"
+ [source]="row.pathForSelection?.split?.('@')?.[0]"
[byId]="false"
[showIconOnly]="true"
>
}
return this.snapshotScheduleService
.getSnapshotScheduleList('/', this.fsName)
- .pipe(map((list) => list.map((l) => ({ ...l, path: `${l.path}@${l.schedule}` }))));
+ .pipe(
+ map((list) =>
+ list.map((l) => ({ ...l, pathForSelection: `${l.path}@${l.schedule}` }))
+ )
+ );
}),
shareReplay(1)
)
);
this.columns = [
- { prop: 'path', name: $localize`Path`, flexGrow: 3, cellTemplate: this.pathTpl },
+ { prop: 'pathForSelection', name: $localize`Path`, flexGrow: 3, cellTemplate: this.pathTpl },
+ { prop: 'path', isHidden: true },
{ prop: 'subvol', name: $localize`Subvolume`, cellTemplate: this.subvolTpl },
{ prop: 'scheduleCopy', name: $localize`Repeat interval` },
{ prop: 'schedule', isHidden: true },
import { CriticalConfirmationModalComponent } from '~/app/shared/components/critical-confirmation-modal/critical-confirmation-modal.component';
import { CephfsSubvolumeGroupService } from '~/app/shared/api/cephfs-subvolume-group.service';
import { CephfsSubvolumeGroup } from '~/app/shared/models/cephfs-subvolume-group.model';
+import { CephfsMountDetailsComponent } from '../cephfs-mount-details/cephfs-mount-details.component';
+import { HealthService } from '~/app/shared/api/health.service';
@Component({
selector: 'cd-cephfs-subvolume-list',
private modalService: ModalService,
private authStorageService: AuthStorageService,
private taskWrapper: TaskWrapperService,
- private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService
+ private cephfsSubvolumeGroupService: CephfsSubvolumeGroupService,
+ private healthService: HealthService
) {
super();
this.permissions = this.authStorageService.getPermissions();
icon: Icons.edit,
click: () => this.openModal(true)
},
+ {
+ name: this.actionLabels.ATTACH,
+ permission: 'read',
+ icon: Icons.bars,
+ disable: () => !this.selection?.hasSelection,
+ click: () => this.showAttachInfo()
+ },
{
name: this.actionLabels.REMOVE,
permission: 'delete',
this.selection = selection;
}
+ showAttachInfo() {
+ const selectedSubVolume = this.selection?.selected?.[0];
+
+ this.healthService.getClusterFsid().subscribe({
+ next: (clusterId: string) => {
+ this.modalRef = this.modalService.show(CephfsMountDetailsComponent, {
+ onSubmit: () => this.modalRef.close(),
+ mountData: {
+ fsId: clusterId,
+ fsName: this.fsName,
+ rootPath: selectedSubVolume.info.path
+ }
+ });
+ }
+ });
+ }
+
openModal(edit = false) {
this.modalService.show(
CephfsSubvolumeFormComponent,
import { DataTableModule } from '../../shared/datatable/datatable.module';
import { CephfsSubvolumeSnapshotsFormComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-form/cephfs-subvolume-snapshots-form.component';
import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component';
+import { CephfsMountDetailsComponent } from './cephfs-mount-details/cephfs-mount-details.component';
@NgModule({
imports: [
CephfsSubvolumeSnapshotsListComponent,
CephfsSnapshotscheduleListComponent,
CephfsSnapshotscheduleFormComponent,
- CephfsSubvolumeSnapshotsFormComponent
+ CephfsSubvolumeSnapshotsFormComponent,
+ CephfsMountDetailsComponent
]
})
export class CephfsModule {}
return this.http.get(`${this.baseURL}/${id}/mds_counters`);
}
+ getFsRootDirectory(id: string) {
+ return this.http.get(`${this.baseURL}/${id}/get_root_directory`);
+ }
+
mkSnapshot(id: number, path: string, name?: string) {
let params = new HttpParams();
params = params.append('path', path);
--- /dev/null
+<ng-container *ngIf="codes.length > 1; else singleCodeBlock">
+ <pre id="bigCodeBlock">
+ <span *ngFor="let code of codes"
+ class="d-flex px-2 py-3 align-items-center justify-content-between text-dark">
+ <span [ngClass]="{'text-wrap': textWrap}">{{code}}</span>
+ <cd-copy-2-clipboard-button
+ [source]="code"
+ [byId]="false"></cd-copy-2-clipboard-button>
+ </span>
+ </pre>
+</ng-container>
+
+<ng-template #singleCodeBlock>
+ <pre class="d-flex px-2 py-3 align-items-center justify-content-between text-dark"
+ id="singleCodeBlock">
+ <span [ngClass]="{'text-wrap': textWrap}">{{codes}}</span>
+ <cd-copy-2-clipboard-button
+ [source]="codes"
+ [byId]="false"></cd-copy-2-clipboard-button>
+ </pre>
+</ng-template>
--- /dev/null
+pre {
+ background-color: var(--gray-200);
+ border-radius: 0.5rem;
+}
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CodeBlockComponent } from './code-block.component';
+import { configureTestBed } from '~/testing/unit-test-helper';
+
+describe('CodeBlockComponent', () => {
+ let component: CodeBlockComponent;
+ let fixture: ComponentFixture<CodeBlockComponent>;
+
+ configureTestBed({
+ declarations: [CodeBlockComponent]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CodeBlockComponent);
+ component = fixture.componentInstance;
+ component.codes = [];
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+
+ it('should show single codeblock if there are only one code', () => {
+ component.codes = ['code'];
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelector('#singleCodeBlock')).not.toBeNull();
+ expect(fixture.nativeElement.querySelector('#bigCodeBlock')).toBeNull();
+ });
+
+ it('should show single codeblock if there are only one code', () => {
+ component.codes = ['code1', 'code2'];
+ fixture.detectChanges();
+ expect(fixture.nativeElement.querySelector('#bigCodeBlock')).not.toBeNull();
+ expect(fixture.nativeElement.querySelector('#singleCodeBlock')).toBeNull();
+ });
+});
--- /dev/null
+import { Component, Input } from '@angular/core';
+
+@Component({
+ selector: 'cd-code-block',
+ templateUrl: './code-block.component.html',
+ styleUrls: ['./code-block.component.scss']
+})
+export class CodeBlockComponent {
+ @Input()
+ codes: string[];
+
+ @Input()
+ textWrap: boolean = false;
+
+ @Input()
+ grayBg: boolean = false;
+}
import { WizardComponent } from './wizard/wizard.component';
import { CardComponent } from './card/card.component';
import { CardRowComponent } from './card-row/card-row.component';
+import { CodeBlockComponent } from './code-block/code-block.component';
import { VerticalNavigationComponent } from './vertical-navigation/vertical-navigation.component';
@NgModule({
ColorClassFromTextPipe,
CardComponent,
CardRowComponent,
+ CodeBlockComponent,
VerticalNavigationComponent
],
providers: [],
CdLabelComponent,
CardComponent,
CardRowComponent,
+ CodeBlockComponent,
VerticalNavigationComponent
]
})
private getText(): string {
const element = document.getElementById(this.source) as HTMLInputElement;
- return element.value;
+ return element?.value || element?.textContent;
}
@HostListener('click')
START_UPGRADE: string;
ACTIVATE: string;
DEACTIVATE: string;
+ ATTACH: string;
constructor() {
/* Create a new item */
this.ACTIVATE = $localize`Activate`;
this.DEACTIVATE = $localize`Deactivate`;
+
+ this.ATTACH = $localize`Attach`;
}
}
- jwt: []
tags:
- Cephfs
+ /api/cephfs/{fs_id}/statfs:
+ get:
+ description: "\n Get the statfs of the specified path.\n :param\
+ \ fs_id: The filesystem identifier.\n :param path: The path of the\
+ \ directory/file.\n :return: Returns a dictionary containing 'bytes',\n\
+ \ 'files' and 'subdirs'.\n :rtype: dict\n "
+ parameters:
+ - description: File System Identifier
+ in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - description: File System Path
+ in: query
+ name: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ schema:
+ properties:
+ bytes:
+ description: ''
+ type: integer
+ files:
+ description: ''
+ type: integer
+ subdirs:
+ description: ''
+ type: integer
+ required:
+ - bytes
+ - files
+ - subdirs
+ type: object
+ description: OK
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ summary: Get Cephfs statfs of the specified path
+ tags:
+ - Cephfs
/api/cephfs/{fs_id}/tree:
delete:
description: "\n Remove a directory.\n :param fs_id: The filesystem\
- jwt: []
tags:
- Cephfs
+ /api/cephfs/{fs_id}/unlink:
+ delete:
+ description: "\n Removes a file, link, or symbolic link.\n :param\
+ \ fs_id: The filesystem identifier.\n :param path: The path of the\
+ \ file or link to unlink.\n "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ - in: query
+ name: path
+ required: true
+ schema:
+ type: string
+ responses:
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '204':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource deleted.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
+ /api/cephfs/{fs_id}/write_to_file:
+ post:
+ description: "\n Write some data to the specified path.\n :param\
+ \ fs_id: The filesystem identifier.\n :param path: The path of the\
+ \ file to write.\n :param buf: The str to write to the buf.\n \
+ \ "
+ parameters:
+ - in: path
+ name: fs_id
+ required: true
+ schema:
+ type: string
+ requestBody:
+ content:
+ application/json:
+ schema:
+ properties:
+ buf:
+ type: string
+ path:
+ type: string
+ required:
+ - path
+ - buf
+ type: object
+ responses:
+ '201':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Resource created.
+ '202':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ type: object
+ description: Operation is still executing. Please check the task queue.
+ '400':
+ description: Operation exception. Please check the response body for details.
+ '401':
+ description: Unauthenticated access. Please login first.
+ '403':
+ description: Unauthorized access. Please check your permissions.
+ '500':
+ description: Unexpected error. Please check the response body for the stack
+ trace.
+ security:
+ - jwt: []
+ tags:
+ - Cephfs
/api/cluster:
get:
parameters: []