)
snapshot['info'] = json.loads(out)
return snapshots
+
+
+@APIRouter('/cephfs/snaphost/schedule', Scope.CEPHFS)
+@APIDoc("Cephfs Snapshot Scheduling API", "CephFSSnapshotSchedule")
+class CephFSSnapshotSchedule(RESTController):
+
+ def list(self, fs: str, path: str = '/', recursive: bool = True):
+ error_code, out, err = mgr.remote('snap_schedule', 'snap_schedule_list',
+ path, recursive, fs, 'plain')
+
+ if len(out) == 0:
+ return []
+
+ snapshot_schedule_list = out.split('\n')
+ output = []
+
+ for snap in snapshot_schedule_list:
+ current_path = snap.strip().split(' ')[0]
+ error_code, status_out, err = mgr.remote('snap_schedule', 'snap_schedule_get',
+ current_path, fs, 'plain')
+ output.append(json.loads(status_out))
+
+ output_json = json.dumps(output)
+
+ if error_code != 0:
+ raise DashboardException(
+ f'Failed to get list of snapshot schedules for path {path}: {err}'
+ )
+
+ return json.loads(output_json)
--- /dev/null
+<ng-container *ngIf="isLoading$ | async">
+ <cd-loading-panel>
+ <span i18n>Loading snapshot schedules...</span>
+ </cd-loading-panel>
+</ng-container>
+
+<ng-template #pathTpl
+ let-row="row">
+ <span
+ class="fw-bold"
+ [ngbTooltip]="fullpathTpl"
+ triggers="click:blur">{{row.path | path}}</span>
+
+ <span *ngIf="row.active; else inactiveStatusTpl">
+ <i [ngClass]="[icons.success, icons.large]"
+ ngbTooltip="{{row.path}} is active"
+ class="text-success"></i>
+ </span>
+
+ <ng-template #inactiveStatusTpl>
+ <i [ngClass]="[icons.warning, icons.large]"
+ class="text-warning"
+ ngbTooltip="{{row.path}} has been deactivated"></i>
+ </ng-template>
+
+ <ng-template #fullpathTpl>
+ <span data-toggle="tooltip"
+ [title]="row.path"
+ class="font-monospace">{{ row.path }}
+ <cd-copy-2-clipboard-button *ngIf="row.path"
+ [source]="row.path"
+ [byId]="false"
+ [showIconOnly]="true">
+ </cd-copy-2-clipboard-button>
+ </span>
+</ng-template>
+
+</ng-template>
+
+<cd-table
+ [data]="snapshotSchedules$ | async"
+ columnMode="flex"
+ [columns]="columns"
+ selectionType="single"
+ [hasDetails]="false"
+ (fetchData)="fetchData()"
+ (updateSelection)="updateSelection($event)"
+>
+ <div class="table-actions btn-toolbar">
+ <cd-table-actions
+ [permission]="permissions.cephfs"
+ [selection]="selection"
+ class="btn-group"
+ [tableActions]="tableActions"
+ >
+ </cd-table-actions>
+ </div>
+</cd-table>
--- /dev/null
+import { ComponentFixture, TestBed } from '@angular/core/testing';
+
+import { CephfsSnapshotscheduleListComponent } from './cephfs-snapshotschedule-list.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: CephfsSnapshotscheduleListComponent;
+ let fixture: ComponentFixture<CephfsSnapshotscheduleListComponent>;
+
+ configureTestBed({
+ declarations: [CephfsSnapshotscheduleListComponent],
+ imports: [HttpClientTestingModule, SharedModule, ToastrModule.forRoot(), RouterTestingModule],
+ providers: [NgbActiveModal]
+ });
+
+ beforeEach(() => {
+ fixture = TestBed.createComponent(CephfsSnapshotscheduleListComponent);
+ component = fixture.componentInstance;
+ fixture.detectChanges();
+ });
+
+ it('should create', () => {
+ expect(component).toBeTruthy();
+ });
+});
--- /dev/null
+import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core';
+import { NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
+import { BehaviorSubject, Observable } from 'rxjs';
+import { finalize, shareReplay, switchMap } from 'rxjs/operators';
+import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
+import { CdForm } from '~/app/shared/forms/cd-form';
+import { CdTableAction } from '~/app/shared/models/cd-table-action';
+import { CdTableColumn } from '~/app/shared/models/cd-table-column';
+import { CdTableFetchDataContext } from '~/app/shared/models/cd-table-fetch-data-context';
+import { CdTableSelection } from '~/app/shared/models/cd-table-selection';
+import { Permissions } from '~/app/shared/models/permissions';
+import { SnapshotSchedule } from '~/app/shared/models/snapshot-schedule';
+import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
+import { ModalService } from '~/app/shared/services/modal.service';
+import { Icons } from '~/app/shared/enum/icons.enum';
+import { CellTemplate } from '~/app/shared/enum/cell-template.enum';
+
+@Component({
+ selector: 'cd-cephfs-snapshotschedule-list',
+ templateUrl: './cephfs-snapshotschedule-list.component.html',
+ styleUrls: ['./cephfs-snapshotschedule-list.component.scss']
+})
+export class CephfsSnapshotscheduleListComponent extends CdForm implements OnInit, OnChanges {
+ @Input() fsName!: string;
+
+ @ViewChild('pathTpl', { static: true })
+ pathTpl: any;
+
+ snapshotSchedules$!: Observable<SnapshotSchedule[]>;
+ subject$ = new BehaviorSubject<SnapshotSchedule[]>([]);
+ isLoading$ = new BehaviorSubject<boolean>(true);
+ columns: CdTableColumn[] = [];
+ tableActions: CdTableAction[] = [];
+ context!: CdTableFetchDataContext;
+ selection = new CdTableSelection();
+ permissions!: Permissions;
+ modalRef!: NgbModalRef;
+ errorMessage: string = '';
+ selectedName: string = '';
+ icons = Icons;
+
+ constructor(
+ private snapshotScheduleService: CephfsSnapshotScheduleService,
+ private authStorageService: AuthStorageService,
+ private modalService: ModalService
+ ) {
+ super();
+ this.permissions = this.authStorageService.getPermissions();
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ if (changes.fsName) {
+ this.subject$.next([]);
+ }
+ }
+
+ ngOnInit(): void {
+ this.snapshotSchedules$ = this.subject$.pipe(
+ switchMap(() =>
+ this.snapshotScheduleService
+ .getSnapshotScheduleList('/', this.fsName)
+ .pipe(finalize(() => this.isLoading$.next(false)))
+ ),
+ shareReplay(1)
+ );
+
+ this.columns = [
+ { prop: 'path', name: $localize`Path`, flexGrow: 3, cellTemplate: this.pathTpl },
+ { prop: 'subvol', name: $localize`Subvolume` },
+ { prop: 'schedule', name: $localize`Repeat interval` },
+ { prop: 'retention', name: $localize`Retention policy` },
+ { prop: 'created_count', name: $localize`Created Count` },
+ { prop: 'pruned_count', name: $localize`Deleted Count` },
+ { prop: 'start', name: $localize`Start time`, cellTransformation: CellTemplate.timeAgo },
+ { prop: 'created', name: $localize`Created`, cellTransformation: CellTemplate.timeAgo }
+ ];
+
+ this.tableActions = [];
+ }
+
+ fetchData() {
+ this.subject$.next([]);
+ }
+
+ updateSelection(selection: CdTableSelection) {
+ this.selection = selection;
+ }
+
+ openModal(edit = false) {
+ this.modalService.show(
+ {},
+ {
+ fsName: 'fs1',
+ isEdit: edit
+ },
+ { size: 'lg' }
+ );
+ }
+}
<ng-container *ngIf="selection">
- <nav ngbNav
- #nav="ngbNav"
- (navChange)="softRefresh()"
- class="nav-tabs"
- cdStatefulTab="cephfs-tabs">
+ <nav
+ ngbNav
+ #nav="ngbNav"
+ (navChange)="softRefresh()"
+ class="nav-tabs"
+ cdStatefulTab="cephfs-tabs"
+ >
<ng-container ngbNavItem="details">
- <a ngbNavLink
- i18n>Details</a>
+ <a
+ ngbNavLink
+ i18n>Details</a>
<ng-template ngbNavContent>
- <cd-cephfs-detail [data]="details">
- </cd-cephfs-detail>
+ <cd-cephfs-detail [data]="details"> </cd-cephfs-detail>
</ng-template>
</ng-container>
<ng-container ngbNavItem="subvolumes">
- <a ngbNavLink
- i18n>Subvolumes</a>
+ <a
+ ngbNavLink
+ i18n>Subvolumes</a>
<ng-template ngbNavContent>
- <cd-cephfs-subvolume-list [fsName]="selection.mdsmap.fs_name"
- [pools]="details.pools"></cd-cephfs-subvolume-list>
+ <cd-cephfs-subvolume-list
+ [fsName]="selection.mdsmap.fs_name"
+ [pools]="details.pools"
+ ></cd-cephfs-subvolume-list>
</ng-template>
</ng-container>
<ng-container ngbNavItem="subvolume-groups">
- <a ngbNavLink
- i18n>Subvolume groups</a>
+ <a
+ ngbNavLink
+ i18n>Subvolume groups</a>
<ng-template ngbNavContent>
- <cd-cephfs-subvolume-group [fsName]="selection.mdsmap.fs_name"
- [pools]="details.pools">
+ <cd-cephfs-subvolume-group
+ [fsName]="selection.mdsmap.fs_name"
+ [pools]="details.pools">
</cd-cephfs-subvolume-group>
</ng-template>
</ng-container>
<ng-container ngbNavItem="snapshots">
- <a ngbNavLink
- i18n>Snapshots</a>
+ <a
+ ngbNavLink
+ i18n>Snapshots</a>
<ng-template ngbNavContent>
<cd-cephfs-subvolume-snapshots-list [fsName]="selection.mdsmap.fs_name">
</cd-cephfs-subvolume-snapshots-list>
</ng-template>
</ng-container>
+ <ng-container ngbNavItem="snapshot-schedules">
+ <a
+ ngbNavLink
+ i18n>Snapshot schedules</a>
+ <ng-template ngbNavContent>
+ <cd-cephfs-snapshotschedule-list
+ [fsName]="selection.mdsmap.fs_name"
+ ></cd-cephfs-snapshotschedule-list>
+ </ng-template>
+ </ng-container>
<ng-container ngbNavItem="clients">
<a ngbNavLink>
<ng-container i18n>Clients</ng-container>
<span class="badge badge-pill badge-tab ms-1">{{ clients.data.length }}</span>
</a>
<ng-template ngbNavContent>
- <cd-cephfs-clients [id]="id"
- [clients]="clients"
- (triggerApiUpdate)="refresh()">
+ <cd-cephfs-clients
+ [id]="id"
+ [clients]="clients"
+ (triggerApiUpdate)="refresh()">
</cd-cephfs-clients>
</ng-template>
</ng-container>
<ng-container ngbNavItem="directories">
- <a ngbNavLink
- i18n>Directories</a>
+ <a
+ ngbNavLink
+ i18n>Directories</a>
<ng-template ngbNavContent>
<cd-cephfs-directories [id]="id"></cd-cephfs-directories>
</ng-template>
</ng-container>
<ng-container ngbNavItem="performance-details">
- <a ngbNavLink
- i18n>Performance Details</a>
+ <a
+ ngbNavLink
+ i18n>Performance Details</a>
<ng-template ngbNavContent>
- <cd-grafana i18n-title
- title="CephFS MDS performance"
- [grafanaPath]="'mds-performance?var-mds_servers=mds.' + grafanaId"
- [type]="'metrics'"
- uid="tbO9LAiZz"
- grafanaStyle="one">
+ <cd-grafana
+ i18n-title
+ title="CephFS MDS performance"
+ [grafanaPath]="'mds-performance?var-mds_servers=mds.' + grafanaId"
+ [type]="'metrics'"
+ uid="tbO9LAiZz"
+ grafanaStyle="one"
+ >
</cd-grafana>
</ng-template>
</ng-container>
import { CephfsSubvolumeGroupComponent } from './cephfs-subvolume-group/cephfs-subvolume-group.component';
import { CephfsSubvolumegroupFormComponent } from './cephfs-subvolumegroup-form/cephfs-subvolumegroup-form.component';
import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapshots-list/cephfs-subvolume-snapshots-list.component';
+import { CephfsSnapshotscheduleListComponent } from './cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component';
+import { DataTableModule } from '../../shared/datatable/datatable.module';
@NgModule({
imports: [
FormsModule,
ReactiveFormsModule,
NgbTypeaheadModule,
- NgbTooltipModule
+ NgbTooltipModule,
+ DataTableModule
],
declarations: [
CephfsDetailComponent,
CephfsDirectoriesComponent,
CephfsSubvolumeGroupComponent,
CephfsSubvolumegroupFormComponent,
- CephfsSubvolumeSnapshotsListComponent
+ CephfsSubvolumeSnapshotsListComponent,
+ CephfsSnapshotscheduleListComponent
]
})
export class CephfsModule {}
--- /dev/null
+import { TestBed } from '@angular/core/testing';
+import { HttpClientTestingModule } from '@angular/common/http/testing';
+
+import { configureTestBed } from '~/testing/unit-test-helper';
+import { CephfsSnapshotScheduleService } from './cephfs-snapshot-schedule.service';
+
+describe('CephfsSnapshotScheduleService', () => {
+ let service: CephfsSnapshotScheduleService;
+
+ configureTestBed({
+ providers: [CephfsSnapshotScheduleService],
+ imports: [HttpClientTestingModule]
+ });
+
+ beforeEach(() => {
+ service = TestBed.inject(CephfsSnapshotScheduleService);
+ });
+
+ it('should be created', () => {
+ expect(service).toBeTruthy();
+ });
+});
--- /dev/null
+import { HttpClient } from '@angular/common/http';
+import { Injectable } from '@angular/core';
+import { Observable } from 'rxjs/internal/Observable';
+import { SnapshotSchedule } from '../models/snapshot-schedule';
+import { map } from 'rxjs/operators';
+
+@Injectable({
+ providedIn: 'root'
+})
+export class CephfsSnapshotScheduleService {
+ baseURL = 'api/cephfs';
+
+ constructor(private http: HttpClient) {}
+
+ getSnapshotScheduleList(
+ path: string,
+ fs: string,
+ recursive = true
+ ): Observable<SnapshotSchedule[]> {
+ return this.http
+ .get<SnapshotSchedule[]>(
+ `${this.baseURL}/snaphost/schedule?path=${path}&fs=${fs}&recursive=${recursive}`
+ )
+ .pipe(
+ map((snapList: SnapshotSchedule[]) =>
+ snapList.map((snapItem: SnapshotSchedule) => ({
+ ...snapItem,
+ status: snapItem.active ? 'Active' : 'Inactive',
+ subvol: snapItem?.subvol || ' - ',
+ retention: Object.values(snapItem.retention)?.length
+ ? Object.entries(snapItem.retention)
+ ?.map?.(([frequency, interval]) => `${interval}${frequency.toLocaleUpperCase()}`)
+ .join(' ')
+ : '-'
+ }))
+ )
+ );
+ }
+}
--- /dev/null
+export interface SnapshotSchedule {
+ fs?: string;
+ subvol?: string;
+ path: string;
+ rel_path?: string;
+ schedule: string;
+ retention?: Record<string, number> | string;
+ start: Date;
+ created: Date;
+ first?: string;
+ last?: string;
+ last_pruned?: string;
+ created_count?: number;
+ pruned_count?: number;
+ active: boolean;
+ status: 'Active' | 'Inactive';
+}
summary: Rename CephFS Volume
tags:
- Cephfs
+ /api/cephfs/snaphost/schedule:
+ get:
+ parameters:
+ - in: query
+ name: fs
+ required: true
+ schema:
+ type: string
+ - default: /
+ in: query
+ name: path
+ schema:
+ type: string
+ - default: true
+ in: query
+ name: recursive
+ schema:
+ type: boolean
+ responses:
+ '200':
+ content:
+ application/vnd.ceph.api.v1.0+json:
+ 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: []
+ tags:
+ - CephFSSnapshotSchedule
/api/cephfs/subvolume:
post:
parameters: []
tags:
- description: Initiate a session with Ceph
name: Auth
+- description: Cephfs Snapshot Scheduling API
+ name: CephFSSnapshotSchedule
- description: CephFS Subvolume Management API
name: CephFSSubvolume
- description: Cephfs Management API