]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: added snap-schedule api and ui list 54750/head
authorIvo Almeida <ialmeida@redhat.com>
Sat, 25 Nov 2023 19:10:35 +0000 (19:10 +0000)
committerIvo Almeida <ialmeida@redhat.com>
Mon, 11 Dec 2023 16:00:15 +0000 (16:00 +0000)
Fixes: https://tracker.ceph.com/issues/63767
Signed-off-by: Ivo Almeida <ialmeida@redhat.com>
src/pybind/mgr/dashboard/controllers/cephfs.py
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-tabs/cephfs-tabs.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs.module.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.spec.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/frontend/src/app/shared/models/snapshot-schedule.ts [new file with mode: 0644]
src/pybind/mgr/dashboard/openapi.yaml

index eb1f764c472bef8c6e4670efb15217f96cdb122b..61b31eb809fd2c9fdacebb447f78d444a7ab7a40 100644 (file)
@@ -863,3 +863,33 @@ class CephFSSubvolumeSnapshots(RESTController):
                     )
                 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)
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html
new file mode 100644 (file)
index 0000000..2e27005
--- /dev/null
@@ -0,0 +1,58 @@
+<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>
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.spec.ts
new file mode 100644 (file)
index 0000000..a20972f
--- /dev/null
@@ -0,0 +1,30 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts b/src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts
new file mode 100644 (file)
index 0000000..d5f24e8
--- /dev/null
@@ -0,0 +1,99 @@
+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' }
+    );
+  }
+}
index 6a50ad2e07869059868e7f2b06df832274e099dc..87268858be503938183b8e7e4fac53de183d66a0 100644 (file)
@@ -1,71 +1,94 @@
 <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>
index cbdb2840867a18206de2575bdaf2b265985b55dd..53544ccd9ed40536d8b910cf4f90f33ada8c779d 100644 (file)
@@ -20,6 +20,8 @@ import { CephfsSubvolumeFormComponent } from './cephfs-subvolume-form/cephfs-sub
 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: [
@@ -32,7 +34,8 @@ import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapsh
     FormsModule,
     ReactiveFormsModule,
     NgbTypeaheadModule,
-    NgbTooltipModule
+    NgbTooltipModule,
+    DataTableModule
   ],
   declarations: [
     CephfsDetailComponent,
@@ -47,7 +50,8 @@ import { CephfsSubvolumeSnapshotsListComponent } from './cephfs-subvolume-snapsh
     CephfsDirectoriesComponent,
     CephfsSubvolumeGroupComponent,
     CephfsSubvolumegroupFormComponent,
-    CephfsSubvolumeSnapshotsListComponent
+    CephfsSubvolumeSnapshotsListComponent,
+    CephfsSnapshotscheduleListComponent
   ]
 })
 export class CephfsModule {}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.spec.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.spec.ts
new file mode 100644 (file)
index 0000000..766b8f3
--- /dev/null
@@ -0,0 +1,22 @@
+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();
+  });
+});
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.ts
new file mode 100644 (file)
index 0000000..ec9f58c
--- /dev/null
@@ -0,0 +1,39 @@
+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(' ')
+              : '-'
+          }))
+        )
+      );
+  }
+}
diff --git a/src/pybind/mgr/dashboard/frontend/src/app/shared/models/snapshot-schedule.ts b/src/pybind/mgr/dashboard/frontend/src/app/shared/models/snapshot-schedule.ts
new file mode 100644 (file)
index 0000000..b1cea74
--- /dev/null
@@ -0,0 +1,17 @@
+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';
+}
index 758ed13f9c5dc902af1ae2067841e87b1d3c3e5c..144c29731ddf9b7d09b5309232af4e9f06937675 100644 (file)
@@ -1758,6 +1758,43 @@ paths:
       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: []
@@ -12859,6 +12896,8 @@ servers:
 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