]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: added subvolume and translations
authorIvo Almeida <ialmeida@redhat.com>
Thu, 8 Feb 2024 15:23:42 +0000 (15:23 +0000)
committerIvo Almeida <ialmeida@redhat.com>
Wed, 14 Feb 2024 13:51:37 +0000 (13:51 +0000)
This commit adds support for subvolume snap scheduling and translations for snap schedule repeat schedules and retention policies

Fixes: https://tracker.ceph.com/issues/64372
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-form/cephfs-snapshotschedule-form.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.ts
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.html
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.scss
src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-list/cephfs-snapshotschedule-list.component.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/api/cephfs-snapshot-schedule.service.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/repeat-frequency.enum.ts
src/pybind/mgr/dashboard/frontend/src/app/shared/enum/retention-frequency.enum.ts
src/pybind/mgr/dashboard/openapi.yaml

index 83e9cbfd8533faf8c1a12a2e3848a71137156d65..ad2cafe0ba8eab725b373f83dfb752c739f28731 100644 (file)
@@ -969,13 +969,16 @@ class CephFSSnapshotSchedule(RESTController):
             )
         return json.loads(output_json)
 
-    def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_policy=None):
+    def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_policy=None,
+               subvol=None, group=None):
         error_code, _, err = mgr.remote('snap_schedule',
                                         'snap_schedule_add',
                                         path,
                                         snap_schedule,
                                         start,
-                                        fs)
+                                        fs,
+                                        subvol,
+                                        group)
 
         if retention_policy:
             retention_policies = retention_policy.split('|')
@@ -999,7 +1002,8 @@ class CephFSSnapshotSchedule(RESTController):
 
         return f'Snapshot schedule for path {path} created successfully'
 
-    def set(self, fs: str, path: str, retention_to_add=None, retention_to_remove=None):
+    def set(self, fs: str, path: str, retention_to_add=None, retention_to_remove=None,
+            subvol=None, group=None):
         def editRetentionPolicies(method, retention_policy):
             if not retention_policy:
                 return
@@ -1014,8 +1018,8 @@ class CephFSSnapshotSchedule(RESTController):
                                                                     retention_spec_or_period,
                                                                     retention_count,
                                                                     fs,
-                                                                    None,
-                                                                    None)
+                                                                    subvol,
+                                                                    group)
                 if error_code_retention != 0:
                     raise DashboardException(
                         f'Failed to add/remove retention policy for path {path}: {err_retention}'
@@ -1027,15 +1031,16 @@ class CephFSSnapshotSchedule(RESTController):
         return f'Retention policies for snapshot schedule on path {path} updated successfully'
 
     @RESTController.Resource('DELETE')
-    def delete_snapshot(self, fs: str, path: str, schedule: str, start: str):
+    def delete_snapshot(self, fs: str, path: str, schedule: str, start: str,
+                        subvol=None, group=None):
         error_code, _, err = mgr.remote('snap_schedule',
                                         'snap_schedule_rm',
                                         path,
                                         schedule,
                                         start,
                                         fs,
-                                        None,
-                                        None)
+                                        subvol,
+                                        group)
         if error_code != 0:
             raise DashboardException(
                 f'Failed to delete snapshot schedule for path {path}: {err}'
@@ -1044,15 +1049,15 @@ class CephFSSnapshotSchedule(RESTController):
         return f'Snapshot schedule for path {path} deleted successfully'
 
     @RESTController.Resource('POST')
-    def deactivate(self, fs: str, path: str, schedule: str, start: str):
+    def deactivate(self, fs: str, path: str, schedule: str, start: str, subvol=None, group=None):
         error_code, _, err = mgr.remote('snap_schedule',
                                         'snap_schedule_deactivate',
                                         path,
                                         schedule,
                                         start,
                                         fs,
-                                        None,
-                                        None)
+                                        subvol,
+                                        group)
         if error_code != 0:
             raise DashboardException(
                 f'Failed to deactivate snapshot schedule for path {path}: {err}'
@@ -1061,15 +1066,15 @@ class CephFSSnapshotSchedule(RESTController):
         return f'Snapshot schedule for path {path} deactivated successfully'
 
     @RESTController.Resource('POST')
-    def activate(self, fs: str, path: str, schedule: str, start: str):
+    def activate(self, fs: str, path: str, schedule: str, start: str, subvol=None, group=None):
         error_code, _, err = mgr.remote('snap_schedule',
                                         'snap_schedule_activate',
                                         path,
                                         schedule,
                                         start,
                                         fs,
-                                        None,
-                                        None)
+                                        subvol,
+                                        group)
         if error_code != 0:
             raise DashboardException(
                 f'Failed to activate snapshot schedule for path {path}: {err}'
index e315e8ab7671dc6e259c5482242e6bff4f785caa..a67293e1138b5de6292230a19dc9172a8b9b9cca 100644 (file)
@@ -36,6 +36,7 @@
                   i18n>A snapshot schedule for this path already exists.</span>
           </div>
         </div>
+
         <!--Start date -->
         <div class="form-group row">
           <label class="cd-col-form-label required"
index 7459a7472c5f26dbc80f82ca94f0e8e5eedefb1e..d14d7debcce94db1ee1325fff75b1b56b0b16749 100644 (file)
@@ -5,6 +5,7 @@ import { uniq } from 'lodash';
 import { Observable, OperatorFunction, of, timer } from 'rxjs';
 import { catchError, debounceTime, distinctUntilChanged, map, switchMap } from 'rxjs/operators';
 import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
+import { CephfsSubvolumeService } from '~/app/shared/api/cephfs-subvolume.service';
 import { DirectoryStoreService } from '~/app/shared/api/directory-store.service';
 import { ActionLabelsI18n, URLVerbs } from '~/app/shared/constants/app.constants';
 import { Icons } from '~/app/shared/enum/icons.enum';
@@ -23,6 +24,7 @@ import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 
 const VALIDATON_TIMER = 300;
 const DEBOUNCE_TIMER = 300;
+const DEFAULT_SUBVOLUME_GROUP = '_nogroup';
 
 @Component({
   selector: 'cd-cephfs-snapshotschedule-form',
@@ -36,12 +38,18 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
   retention!: string;
   start!: string;
   status!: string;
+  subvol!: string;
+  group!: string;
   id!: number;
   isEdit = false;
   icons = Icons;
   repeatFrequencies = Object.entries(RepeatFrequency);
   retentionFrequencies = Object.entries(RetentionFrequency);
   retentionPoliciesToRemove: RetentionPolicy[] = [];
+  isDefaultSubvolumeGroup = false;
+  subvolumeGroup!: string;
+  subvolume!: string;
+  isSubvolume = false;
 
   currentTime!: NgbTimeStruct;
   minDate!: NgbDateStruct;
@@ -59,7 +67,8 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
     private snapScheduleService: CephfsSnapshotScheduleService,
     private taskWrapper: TaskWrapperService,
     private cd: ChangeDetectorRef,
-    public directoryStore: DirectoryStoreService
+    public directoryStore: DirectoryStoreService,
+    private subvolumeService: CephfsSubvolumeService
   ) {
     super();
     this.resource = $localize`Snapshot schedule`;
@@ -82,6 +91,25 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
     this.directoryStore.loadDirectories(this.id, '/', 3);
     this.createForm();
     this.isEdit ? this.populateForm() : this.loadingReady();
+    this.snapScheduleForm.get('directory').valueChanges.subscribe({
+      next: (value: string) => {
+        this.subvolumeGroup = value?.split?.('/')?.[2];
+        this.subvolume = value?.split?.('/')?.[3];
+        this.subvolumeService
+          .exists(
+            this.subvolume,
+            this.fsName,
+            this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP ? '' : this.subvolumeGroup
+          )
+          .subscribe({
+            next: (exists: boolean) => {
+              this.isSubvolume = exists;
+              this.isDefaultSubvolumeGroup =
+                exists && this.subvolumeGroup === DEFAULT_SUBVOLUME_GROUP;
+            }
+          });
+      }
+    });
   }
 
   get retentionPolicies() {
@@ -149,6 +177,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
     this.snapScheduleForm = new CdFormGroup(
       {
         directory: new FormControl(undefined, {
+          updateOn: 'blur',
           validators: [Validators.required]
         }),
         startDate: new FormControl(this.minDate, {
@@ -234,6 +263,8 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
       const updateObj = {
         fs: this.fsName,
         path: this.path,
+        subvol: this.subvol,
+        group: this.group,
         retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
         retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
       };
@@ -262,9 +293,15 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
       };
 
       const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
-      if (retentionPoliciesValues) {
-        snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+
+      if (retentionPoliciesValues) snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+
+      if (this.isSubvolume) snapScheduleObj['subvol'] = this.subvolume;
+
+      if (this.isSubvolume && !this.isDefaultSubvolumeGroup) {
+        snapScheduleObj['group'] = this.subvolumeGroup;
       }
+
       this.taskWrapper
         .wrapTaskAroundCall({
           task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
index f2e93cc742c7af957c7103f9053ad3c04f134a08..f26f63e755a6e58227b16d294bf6c0e1e0352e78 100644 (file)
     class="fw-bold"
     [ngbTooltip]="fullpathTpl"
     triggers="click:blur">
-    {{ row.path | path }}
+    {{ row.path?.split?.("@")?.[0] | path }}
   </span>
 
   <span
   *ngIf="row.active; else inactiveStatusTpl">
     <i
       [ngClass]="[icons.success, icons.large]"
-      ngbTooltip="{{ row.path }} is active"
+      ngbTooltip="{{ row.path?.split?.('@')?.[0] }} is active"
       class="text-success"
     ></i>
   </span>
@@ -37,7 +37,7 @@
     <i
       [ngClass]="[icons.warning, icons.large]"
       class="text-warning"
-      ngbTooltip="{{ row.path }} has been deactivated"
+      ngbTooltip="{{ row.path?.split?.('@')?.[0] }} has been deactivated"
     ></i>
   </ng-template>
 
       data-toggle="tooltip"
       [title]="row.path"
       class="font-monospace"
-      >{{ row.path }}
+      >{{ row.path?.split?.("@")?.[0] }}
       <cd-copy-2-clipboard-button
         *ngIf="row.path"
-        [source]="row.path"
+        [source]="row.path?.split?.('@')?.[0]"
         [byId]="false"
         [showIconOnly]="true"
       >
   </ng-template>
 </ng-template>
 
+<ng-template
+  #retentionTpl
+  let-row="row">
+  <ul *ngIf="row.retentionCopy.length; else noDataTpl">
+    <li *ngFor="let ret of row.retentionCopy">{{ ret }}</li>
+  </ul>
+</ng-template>
+
+<ng-template
+  #subvolTpl
+  let-row="row">
+  <span *ngIf="row.subvol; else noDataTpl">
+    {{row.subvol}}
+  </span>
+</ng-template>
+
+
+
+<ng-template #noDataTpl>-</ng-template>
+
 <cd-table
   [data]="snapshotSchedules$ | async"
   *ngIf="snapScheduleModuleStatus$ | async"
index 58d3a0cc056afbc4a40210baf8819c3d7689354a..581ee6e2fa3ae9cd2a0128bd68c276ef9e9d4e0f 100644 (file)
@@ -46,6 +46,12 @@ export class CephfsSnapshotscheduleListComponent
   @ViewChild('pathTpl', { static: true })
   pathTpl: any;
 
+  @ViewChild('retentionTpl', { static: true })
+  retentionTpl: any;
+
+  @ViewChild('subvolTpl', { static: true })
+  subvolTpl: any;
+
   @BlockUI()
   blockUI: NgBlockUI;
 
@@ -122,7 +128,9 @@ export class CephfsSnapshotscheduleListComponent
             if (!status) {
               return of([]);
             }
-            return this.snapshotScheduleService.getSnapshotScheduleList('/', this.fsName);
+            return this.snapshotScheduleService
+              .getSnapshotScheduleList('/', this.fsName)
+              .pipe(map((list) => list.map((l) => ({ ...l, path: `${l.path}@${l.schedule}` }))));
           }),
           shareReplay(1)
         )
@@ -131,9 +139,11 @@ export class CephfsSnapshotscheduleListComponent
 
     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: 'subvol', name: $localize`Subvolume`, cellTemplate: this.subvolTpl },
+      { prop: 'scheduleCopy', name: $localize`Repeat interval` },
+      { prop: 'schedule', isHidden: true },
+      { prop: 'retentionCopy', name: $localize`Retention policy`, cellTemplate: this.retentionTpl },
+      { prop: 'retention', isHidden: true },
       { prop: 'created_count', name: $localize`Created Count` },
       { prop: 'pruned_count', name: $localize`Deleted Count` },
       { prop: 'start', name: $localize`Start time`, cellTransformation: CellTemplate.timeAgo },
@@ -230,7 +240,7 @@ export class CephfsSnapshotscheduleListComponent
   }
 
   deactivateSnapshotSchedule() {
-    const { path, start, fs, schedule } = this.selection.first();
+    const { path, start, fs, schedule, subvol, group } = this.selection.first();
 
     this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
       itemDescription: $localize`snapshot schedule`,
@@ -244,14 +254,16 @@ export class CephfsSnapshotscheduleListComponent
             path,
             schedule,
             start,
-            fs
+            fs,
+            subvol,
+            group
           })
         })
     });
   }
 
   activateSnapshotSchedule() {
-    const { path, start, fs, schedule } = this.selection.first();
+    const { path, start, fs, schedule, subvol, group } = this.selection.first();
 
     this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
       itemDescription: $localize`snapshot schedule`,
@@ -265,14 +277,16 @@ export class CephfsSnapshotscheduleListComponent
             path,
             schedule,
             start,
-            fs
+            fs,
+            subvol,
+            group
           })
         })
     });
   }
 
   deleteSnapshotSchedule() {
-    const { path, start, fs, schedule } = this.selection.first();
+    const { path, start, fs, schedule, subvol, group } = this.selection.first();
 
     this.modalRef = this.modalService.show(CriticalConfirmationModalComponent, {
       itemDescription: $localize`snapshot schedule`,
@@ -285,7 +299,9 @@ export class CephfsSnapshotscheduleListComponent
             path,
             schedule,
             start,
-            fs
+            fs,
+            subvol,
+            group
           })
         })
     });
index 9e07f6057ac1d4d2e84e180628a5a43476c7551a..93c04dc38ed953a92f41a3e6e7fa9ab4213853b4 100644 (file)
@@ -5,7 +5,12 @@ import { catchError, map } from 'rxjs/operators';
 import { intersection, isEqual, uniqWith } from 'lodash';
 import { SnapshotSchedule } from '../models/snapshot-schedule';
 import { of } from 'rxjs';
-import { RepeatFrequency } from '../enum/repeat-frequency.enum';
+import {
+  RepeaFrequencyPlural,
+  RepeaFrequencySingular,
+  RepeatFrequency
+} from '../enum/repeat-frequency.enum';
+import { RetentionFrequencyCopy } from '../enum/retention-frequency.enum';
 
 @Injectable({
   providedIn: 'root'
@@ -43,12 +48,14 @@ export class CephfsSnapshotScheduleService {
     );
   }
 
-  delete({ fs, path, schedule, start }: Record<string, any>): Observable<any> {
-    return this.http.delete(
-      `${this.baseURL}/snapshot/schedule/${fs}/${encodeURIComponent(
-        path
-      )}/delete_snapshot?schedule=${schedule}&start=${encodeURIComponent(start)}`
-    );
+  delete({ fs, path, schedule, start, subvol, group }: Record<string, any>): Observable<any> {
+    let deleteUrl = `${this.baseURL}/snapshot/schedule/${fs}/${encodeURIComponent(
+      path
+    )}/delete_snapshot?schedule=${schedule}&start=${encodeURIComponent(start)}`;
+    if (subvol && group) {
+      deleteUrl += `&subvol=${encodeURIComponent(subvol)}&group=${encodeURIComponent(group)}`;
+    }
+    return this.http.delete(deleteUrl);
   }
 
   checkScheduleExists(
@@ -122,8 +129,10 @@ export class CephfsSnapshotScheduleService {
         uniqWith(
           snapList.map((snapItem: SnapshotSchedule) => ({
             ...snapItem,
+            scheduleCopy: this.parseScheduleCopy(snapItem.schedule),
             status: snapItem.active ? 'Active' : 'Inactive',
-            subvol: snapItem?.subvol || ' - ',
+            subvol: snapItem?.subvol,
+            retentionCopy: this.parseRetentionCopy(snapItem?.retention),
             retention: Object.values(snapItem?.retention || [])?.length
               ? Object.entries(snapItem.retention)
                   ?.map?.(([frequency, interval]) => `${interval}${frequency.toLocaleUpperCase()}`)
@@ -135,4 +144,20 @@ export class CephfsSnapshotScheduleService {
       )
     );
   }
+
+  parseScheduleCopy(schedule: string): string {
+    const scheduleArr = schedule.split('');
+    const interval = Number(scheduleArr.filter((x) => !isNaN(Number(x))).join(''));
+    const frequencyUnit = scheduleArr[scheduleArr.length - 1];
+    const frequency =
+      interval > 1 ? RepeaFrequencyPlural[frequencyUnit] : RepeaFrequencySingular[frequencyUnit];
+    return $localize`Every ${interval > 1 ? interval + ' ' : ''}${frequency}`;
+  }
+
+  parseRetentionCopy(retention: string | Record<string, number>): string[] {
+    if (!retention) return ['-'];
+    return Object.entries(retention).map(([frequency, interval]) =>
+      $localize`${interval} ${RetentionFrequencyCopy[frequency]}`.toLocaleLowerCase()
+    );
+  }
 }
index db3563ed2b47c390f0b65a8e5bbb6b33717201c8..06fb1c3afc0c028d5af5b3ea81a59b2dd2ba4015 100644 (file)
@@ -3,3 +3,15 @@ export enum RepeatFrequency {
   Daily = 'd',
   Weekly = 'w'
 }
+
+export enum RepeaFrequencySingular {
+  h = 'hour',
+  d = 'day',
+  w = 'week'
+}
+
+export enum RepeaFrequencyPlural {
+  h = 'hours',
+  d = 'days',
+  w = 'weeks'
+}
index 44714dac94638dfdebe5d26f1ceacc2bad2b1e49..193418a1783c687eb4455ca24a1771b44ae9e3bc 100644 (file)
@@ -6,3 +6,12 @@ export enum RetentionFrequency {
   Yearly = 'y',
   'lastest snapshots' = 'n'
 }
+
+export enum RetentionFrequencyCopy {
+  h = 'Hourly',
+  d = 'Daily',
+  w = 'Weekly',
+  m = 'Monthly',
+  y = 'Yearly',
+  n = 'lastest snapshots'
+}
index 55269d6348ee2d2a88812cd9fef0ae345066804e..8d095d86c4e3bb2bcaaef483a6d8b4efed384e88 100644 (file)
@@ -1745,6 +1745,8 @@ paths:
               properties:
                 fs:
                   type: string
+                group:
+                  type: string
                 path:
                   type: string
                 retention_policy:
@@ -1753,6 +1755,8 @@ paths:
                   type: string
                 start:
                   type: string
+                subvol:
+                  type: string
               required:
               - fs
               - path
@@ -1838,10 +1842,14 @@ paths:
           application/json:
             schema:
               properties:
+                group:
+                  type: string
                 retention_to_add:
                   type: string
                 retention_to_remove:
                   type: string
+                subvol:
+                  type: string
               type: object
       responses:
         '200':
@@ -1885,10 +1893,14 @@ paths:
           application/json:
             schema:
               properties:
+                group:
+                  type: string
                 schedule:
                   type: string
                 start:
                   type: string
+                subvol:
+                  type: string
               required:
               - schedule
               - start
@@ -1935,10 +1947,14 @@ paths:
           application/json:
             schema:
               properties:
+                group:
+                  type: string
                 schedule:
                   type: string
                 start:
                   type: string
+                subvol:
+                  type: string
               required:
               - schedule
               - start
@@ -1990,6 +2006,16 @@ paths:
         required: true
         schema:
           type: string
+      - allowEmptyValue: true
+        in: query
+        name: subvol
+        schema:
+          type: string
+      - allowEmptyValue: true
+        in: query
+        name: group
+        schema:
+          type: string
       responses:
         '202':
           content: