]> git.apps.os.sepia.ceph.com Git - ceph.git/commitdiff
mgr/dashboard: snapshot schedule edit form
authorIvo Almeida <ialmeida@redhat.com>
Thu, 1 Feb 2024 18:15:19 +0000 (18:15 +0000)
committerIvo Almeida <ialmeida@redhat.com>
Wed, 14 Feb 2024 13:50:45 +0000 (13:50 +0000)
Fixes: https://tracker.ceph.com/issues/64331
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.ts
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/services/task-message.service.ts
src/pybind/mgr/dashboard/openapi.yaml

index 86f112522c8fc7821c594aa465e425e5282aff20..1437962723ff2aa795894544ae5771a4e4131695 100644 (file)
@@ -1,10 +1,11 @@
 # -*- coding: utf-8 -*-
+# pylint: disable=too-many-lines
 import errno
 import json
 import logging
 import os
 from collections import defaultdict
-from typing import Any, Dict
+from typing import Any, Dict, List
 
 import cephfs
 import cherrypy
@@ -952,7 +953,7 @@ class CephFSSnapshotSchedule(RESTController):
             return []
 
         snapshot_schedule_list = out.split('\n')
-        output: list[Any] = []
+        output: List[Any] = []
 
         for snap in snapshot_schedule_list:
             current_path = snap.strip().split(' ')[0]
@@ -997,3 +998,30 @@ 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 editRetentionPolicies(method, retention_policy):
+            if not retention_policy:
+                return
+
+            retention_policies = retention_policy.split('|')
+            for retention in retention_policies:
+                retention_count = retention.split('-')[0]
+                retention_spec_or_period = retention.split('-')[1]
+                error_code_retention, _, err_retention = mgr.remote('snap_schedule',
+                                                                    method,
+                                                                    path,
+                                                                    retention_spec_or_period,
+                                                                    retention_count,
+                                                                    fs,
+                                                                    None,
+                                                                    None)
+                if error_code_retention != 0:
+                    raise DashboardException(
+                        f'Failed to add/remove retention policy for path {path}: {err_retention}'
+                    )
+
+        editRetentionPolicies('snap_schedule_retention_rm', retention_to_remove)
+        editRetentionPolicies('snap_schedule_retention_add', retention_to_add)
+
+        return f'Retention policies for snapshot schedule on path {path} updated successfully'
index 41e55c72599ac62ee02e52e4f4c4bdddfe9733b6..7459a7472c5f26dbc80f82ca94f0e8e5eedefb1e 100644 (file)
@@ -14,7 +14,11 @@ import { CdForm } from '~/app/shared/forms/cd-form';
 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
 import { CdTableColumn } from '~/app/shared/models/cd-table-column';
 import { FinishedTask } from '~/app/shared/models/finished-task';
-import { RetentionPolicy, SnapshotScheduleFormValue } from '~/app/shared/models/snapshot-schedule';
+import {
+  RetentionPolicy,
+  SnapshotSchedule,
+  SnapshotScheduleFormValue
+} from '~/app/shared/models/snapshot-schedule';
 import { TaskWrapperService } from '~/app/shared/services/task-wrapper.service';
 
 const VALIDATON_TIMER = 300;
@@ -27,11 +31,17 @@ const DEBOUNCE_TIMER = 300;
 })
 export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnInit {
   fsName!: string;
+  path!: string;
+  schedule!: string;
+  retention!: string;
+  start!: string;
+  status!: string;
   id!: number;
   isEdit = false;
   icons = Icons;
   repeatFrequencies = Object.entries(RepeatFrequency);
   retentionFrequencies = Object.entries(RetentionFrequency);
+  retentionPoliciesToRemove: RetentionPolicy[] = [];
 
   currentTime!: NgbTimeStruct;
   minDate!: NgbDateStruct;
@@ -71,7 +81,7 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
     this.action = this.actionLabels.CREATE;
     this.directoryStore.loadDirectories(this.id, '/', 3);
     this.createForm();
-    this.loadingReady();
+    this.isEdit ? this.populateForm() : this.loadingReady();
   }
 
   get retentionPolicies() {
@@ -91,6 +101,50 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
       )
     );
 
+  populateForm() {
+    this.action = this.actionLabels.EDIT;
+    this.snapScheduleService.getSnapshotSchedule(this.path, this.fsName, false).subscribe({
+      next: (response: SnapshotSchedule[]) => {
+        const first = response.find((x) => x.path === this.path);
+        this.snapScheduleForm.get('directory').disable();
+        this.snapScheduleForm.get('directory').setValue(first.path);
+        this.snapScheduleForm.get('startDate').disable();
+        this.snapScheduleForm.get('startDate').setValue({
+          year: new Date(first.start).getUTCFullYear(),
+          month: new Date(first.start).getUTCMonth() + 1,
+          day: new Date(first.start).getUTCDate()
+        });
+        this.snapScheduleForm.get('startTime').disable();
+        this.snapScheduleForm.get('startTime').setValue({
+          hour: new Date(first.start).getUTCHours(),
+          minute: new Date(first.start).getUTCMinutes(),
+          second: new Date(first.start).getUTCSeconds()
+        });
+        this.snapScheduleForm.get('repeatInterval').disable();
+        this.snapScheduleForm.get('repeatInterval').setValue(first.schedule.split('')?.[0]);
+        this.snapScheduleForm.get('repeatFrequency').disable();
+        this.snapScheduleForm.get('repeatFrequency').setValue(first.schedule.split('')?.[1]);
+
+        // retention policies
+        first.retention &&
+          Object.entries(first.retention).forEach(([frequency, interval], idx) => {
+            const freqKey = Object.keys(RetentionFrequency)[
+              Object.values(RetentionFrequency).indexOf(frequency as any)
+            ];
+            this.retentionPolicies.push(
+              new FormGroup({
+                retentionInterval: new FormControl(interval),
+                retentionFrequency: new FormControl(RetentionFrequency[freqKey])
+              })
+            );
+            this.retentionPolicies.controls[idx].get('retentionInterval').disable();
+            this.retentionPolicies.controls[idx].get('retentionFrequency').disable();
+          });
+        this.loadingReady();
+      }
+    });
+  }
+
   createForm() {
     this.snapScheduleForm = new CdFormGroup(
       {
@@ -128,11 +182,19 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
   }
 
   removeRetentionPolicy(idx: number) {
+    if (this.isEdit && this.retentionPolicies.at(idx).disabled) {
+      const values = this.retentionPolicies.at(idx).value as RetentionPolicy;
+      this.retentionPoliciesToRemove.push(values);
+    }
     this.retentionPolicies.removeAt(idx);
+    this.retentionPolicies.controls.forEach((x) =>
+      x.get('retentionFrequency').updateValueAndValidity()
+    );
     this.cd.detectChanges();
   }
 
   parseDatetime(date: NgbDateStruct, time?: NgbTimeStruct): string {
+    if (!date || !time) return null;
     return `${date.year}-${date.month}-${date.day}T${time.hour || '00'}:${time.minute || '00'}:${
       time.second || '00'
     }`;
@@ -156,33 +218,69 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
 
     const values = this.snapScheduleForm.value as SnapshotScheduleFormValue;
 
-    const snapScheduleObj = {
-      fs: this.fsName,
-      path: values.directory,
-      snap_schedule: this.parseSchedule(values.repeatInterval, values.repeatFrequency),
-      start: this.parseDatetime(values.startDate, values.startTime)
-    };
+    if (this.isEdit) {
+      const retentionPoliciesToAdd = (this.snapScheduleForm.get(
+        'retentionPolicies'
+      ) as FormArray).controls
+        ?.filter(
+          (ctrl) =>
+            !ctrl.get('retentionInterval').disabled && !ctrl.get('retentionFrequency').disabled
+        )
+        .map((ctrl) => ({
+          retentionInterval: ctrl.get('retentionInterval').value,
+          retentionFrequency: ctrl.get('retentionFrequency').value
+        }));
 
-    const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
-    if (retentionPoliciesValues) {
-      snapScheduleObj['retention_policy'] = retentionPoliciesValues;
-    }
+      const updateObj = {
+        fs: this.fsName,
+        path: this.path,
+        retention_to_add: this.parseRetentionPolicies(retentionPoliciesToAdd) || null,
+        retention_to_remove: this.parseRetentionPolicies(this.retentionPoliciesToRemove) || null
+      };
 
-    this.taskWrapper
-      .wrapTaskAroundCall({
-        task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
-          path: snapScheduleObj.path
-        }),
-        call: this.snapScheduleService.create(snapScheduleObj)
-      })
-      .subscribe({
-        error: () => {
-          this.snapScheduleForm.setErrors({ cdSubmitButton: true });
-        },
-        complete: () => {
-          this.activeModal.close();
-        }
-      });
+      this.taskWrapper
+        .wrapTaskAroundCall({
+          task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.EDIT, {
+            path: this.path
+          }),
+          call: this.snapScheduleService.update(updateObj)
+        })
+        .subscribe({
+          error: () => {
+            this.snapScheduleForm.setErrors({ cdSubmitButton: true });
+          },
+          complete: () => {
+            this.activeModal.close();
+          }
+        });
+    } else {
+      const snapScheduleObj = {
+        fs: this.fsName,
+        path: values.directory,
+        snap_schedule: this.parseSchedule(values?.repeatInterval, values?.repeatFrequency),
+        start: this.parseDatetime(values?.startDate, values?.startTime)
+      };
+
+      const retentionPoliciesValues = this.parseRetentionPolicies(values?.retentionPolicies);
+      if (retentionPoliciesValues) {
+        snapScheduleObj['retention_policy'] = retentionPoliciesValues;
+      }
+      this.taskWrapper
+        .wrapTaskAroundCall({
+          task: new FinishedTask('cephfs/snapshot/schedule/' + URLVerbs.CREATE, {
+            path: snapScheduleObj.path
+          }),
+          call: this.snapScheduleService.create(snapScheduleObj)
+        })
+        .subscribe({
+          error: () => {
+            this.snapScheduleForm.setErrors({ cdSubmitButton: true });
+          },
+          complete: () => {
+            this.activeModal.close();
+          }
+        });
+    }
   }
 
   validateSchedule() {
@@ -190,6 +288,11 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
       const directory = frm.get('directory');
       const repeatFrequency = frm.get('repeatFrequency');
       const repeatInterval = frm.get('repeatInterval');
+
+      if (this.isEdit) {
+        return of(null);
+      }
+
       return timer(VALIDATON_TIMER).pipe(
         switchMap(() =>
           this.snapScheduleService
@@ -239,7 +342,12 @@ export class CephfsSnapshotscheduleFormComponent extends CdForm implements OnIni
             return null;
           }
           return this.snapScheduleService
-            .checkRetentionPolicyExists(frm.get('directory').value, this.fsName, retentionList)
+            .checkRetentionPolicyExists(
+              frm.get('directory').value,
+              this.fsName,
+              retentionList,
+              this.retentionPoliciesToRemove?.map?.((rp) => rp.retentionFrequency) || []
+            )
             .pipe(
               map(({ exists, errorIndex }) => {
                 if (exists) {
index 6b406cfc1712f7bfff02a0d3d790feea10cf8902..b6b52a15c99d2aaabf6c9a7dfeac2eeffcc962d0 100644 (file)
@@ -121,6 +121,12 @@ export class CephfsSnapshotscheduleListComponent
         name: this.actionLables.CREATE,
         permission: 'create',
         icon: Icons.add,
+        click: () => this.openModal(false)
+      },
+      {
+        name: this.actionLables.EDIT,
+        permission: 'update',
+        icon: Icons.edit,
         click: () => this.openModal(true)
       }
     ];
@@ -145,6 +151,10 @@ export class CephfsSnapshotscheduleListComponent
         fsName: this.fsName,
         id: this.id,
         path: this.selection?.first()?.path,
+        schedule: this.selection?.first()?.schedule,
+        retention: this.selection?.first()?.retention,
+        start: this.selection?.first()?.start,
+        status: this.selection?.first()?.status,
         isEdit: edit
       },
       { size: 'lg' }
index 0666bb179e834dacdfc7c05b3d5250206b0cf53b..0719089c249f2590763f04e21eff29ebd1db5d3a 100644 (file)
@@ -19,6 +19,14 @@ export class CephfsSnapshotScheduleService {
     return this.http.post(`${this.baseURL}/snapshot/schedule`, data, { observe: 'response' });
   }
 
+  update(data: Record<string, any>): Observable<any> {
+    return this.http.put(
+      `${this.baseURL}/snapshot/schedule/${data.fs}/${encodeURIComponent(data.path)}`,
+      data,
+      { observe: 'response' }
+    );
+  }
+
   checkScheduleExists(
     path: string,
     fs: string,
@@ -41,15 +49,21 @@ export class CephfsSnapshotScheduleService {
   checkRetentionPolicyExists(
     path: string,
     fs: string,
-    retentionFrequencies: string[]
+    retentionFrequencies: string[],
+    retentionFrequenciesRemoved: string[] = []
   ): Observable<{ exists: boolean; errorIndex: number }> {
-    return this.getList(path, fs, false).pipe(
+    return this.getSnapshotSchedule(path, fs, false).pipe(
       map((response) => {
         let errorIndex = -1;
         let exists = false;
         const index = response.findIndex((x) => x.path === path);
         const result = retentionFrequencies?.length
-          ? intersection(Object.keys(response?.[index]?.retention), retentionFrequencies)
+          ? intersection(
+              Object.keys(response?.[index]?.retention).filter(
+                (v) => !retentionFrequenciesRemoved.includes(v)
+              ),
+              retentionFrequencies
+            )
           : [];
         exists = !!result?.length;
         result?.forEach((r) => (errorIndex = retentionFrequencies.indexOf(r)));
@@ -62,10 +76,10 @@ export class CephfsSnapshotScheduleService {
     );
   }
 
-  private getList(path: string, fs: string, recursive = true): Observable<SnapshotSchedule[]> {
+  getSnapshotSchedule(path: string, fs: string, recursive = true): Observable<SnapshotSchedule[]> {
     return this.http
       .get<SnapshotSchedule[]>(
-        `${this.baseURL}/snapshot/schedule?path=${path}&fs=${fs}&recursive=${recursive}`
+        `${this.baseURL}/snapshot/schedule/${fs}?path=${path}&recursive=${recursive}`
       )
       .pipe(
         catchError(() => {
@@ -79,7 +93,7 @@ export class CephfsSnapshotScheduleService {
     fs: string,
     recursive = true
   ): Observable<SnapshotSchedule[]> {
-    return this.getList(path, fs, recursive).pipe(
+    return this.getSnapshotSchedule(path, fs, recursive).pipe(
       map((snapList: SnapshotSchedule[]) =>
         uniqWith(
           snapList.map((snapItem: SnapshotSchedule) => ({
index f631842919c0f8beda65d26d204d91ee038d587c..84e31efea013777d0923a388594e5cee7d186e72 100644 (file)
@@ -390,6 +390,9 @@ export class TaskMessageService {
     ),
     'cephfs/snapshot/schedule/create': this.newTaskMessage(this.commonOperations.add, (metadata) =>
       this.snapshotSchedule(metadata)
+    ),
+    'cephfs/snapshot/schedule/edit': this.newTaskMessage(this.commonOperations.update, (metadata) =>
+      this.snapshotSchedule(metadata)
     )
   };
 
index ed2dcca25db09a21a76d45ea9d5f052f0e12c88e..181e5ecd5005dc238cd0d9c71ba04a3c90414c87 100644 (file)
@@ -1736,9 +1736,57 @@ paths:
       tags:
       - Cephfs
   /api/cephfs/snapshot/schedule:
+    post:
+      parameters: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              properties:
+                fs:
+                  type: string
+                path:
+                  type: string
+                retention_policy:
+                  type: string
+                snap_schedule:
+                  type: string
+                start:
+                  type: string
+              required:
+              - fs
+              - path
+              - snap_schedule
+              - start
+              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:
+      - CephFSSnapshotSchedule
+  /api/cephfs/snapshot/schedule/{fs}:
     get:
       parameters:
-      - in: query
+      - in: path
         name: fs
         required: true
         schema:
@@ -1772,35 +1820,35 @@ paths:
       - jwt: []
       tags:
       - CephFSSnapshotSchedule
-    post:
-      parameters: []
+  /api/cephfs/snapshot/schedule/{fs}/{path}:
+    put:
+      parameters:
+      - in: path
+        name: fs
+        required: true
+        schema:
+          type: string
+      - in: path
+        name: path
+        required: true
+        schema:
+          type: string
       requestBody:
         content:
           application/json:
             schema:
               properties:
-                fs:
-                  type: string
-                path:
-                  type: string
-                retention_policy:
-                  type: string
-                snap_schedule:
+                retention_to_add:
                   type: string
-                start:
+                retention_to_remove:
                   type: string
-              required:
-              - fs
-              - path
-              - snap_schedule
-              - start
               type: object
       responses:
-        '201':
+        '200':
           content:
             application/vnd.ceph.api.v1.0+json:
               type: object
-          description: Resource created.
+          description: Resource updated.
         '202':
           content:
             application/vnd.ceph.api.v1.0+json: