Skip to content

Commit 9545549

Browse files
authored
Merge pull request ceph#54900 from ivoalmeida/snapshot-schedule-create
added snap schedule form Reviewed-by: Pedro Gonzalez Gomez <[email protected]> Reviewed-by: Ankush Behl <[email protected]> Reviewed-by: Nizamudeen A <[email protected]>
2 parents 52a1268 + d7c9691 commit 9545549

16 files changed

+736
-25
lines changed

src/pybind/mgr/dashboard/controllers/cephfs.py

Lines changed: 35 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -941,31 +941,59 @@ def create(self, vol_name: str, subvol_name: str, snap_name: str, clone_name: st
941941
return f'Clone {clone_name} created successfully'
942942

943943

944-
@APIRouter('/cephfs/snaphost/schedule', Scope.CEPHFS)
944+
@APIRouter('/cephfs/snapshot/schedule', Scope.CEPHFS)
945945
@APIDoc("Cephfs Snapshot Scheduling API", "CephFSSnapshotSchedule")
946946
class CephFSSnapshotSchedule(RESTController):
947947

948948
def list(self, fs: str, path: str = '/', recursive: bool = True):
949949
error_code, out, err = mgr.remote('snap_schedule', 'snap_schedule_list',
950-
path, recursive, fs, 'plain')
951-
950+
path, recursive, fs, None, None, 'plain')
952951
if len(out) == 0:
953952
return []
954953

955954
snapshot_schedule_list = out.split('\n')
956-
output = []
955+
output: list[Any] = []
957956

958957
for snap in snapshot_schedule_list:
959958
current_path = snap.strip().split(' ')[0]
960959
error_code, status_out, err = mgr.remote('snap_schedule', 'snap_schedule_get',
961-
current_path, fs, 'plain')
962-
output.append(json.loads(status_out))
960+
current_path, fs, None, None, 'json')
961+
output = output + json.loads(status_out)
963962

964963
output_json = json.dumps(output)
965964

966965
if error_code != 0:
967966
raise DashboardException(
968967
f'Failed to get list of snapshot schedules for path {path}: {err}'
969968
)
970-
971969
return json.loads(output_json)
970+
971+
def create(self, fs: str, path: str, snap_schedule: str, start: str, retention_policy=None):
972+
error_code, _, err = mgr.remote('snap_schedule',
973+
'snap_schedule_add',
974+
path,
975+
snap_schedule,
976+
start,
977+
fs)
978+
979+
if retention_policy:
980+
retention_policies = retention_policy.split('|')
981+
for retention in retention_policies:
982+
retention_count = retention.split('-')[0]
983+
retention_spec_or_period = retention.split('-')[1]
984+
error_code_retention, _, err_retention = mgr.remote('snap_schedule',
985+
'snap_schedule_retention_add',
986+
path,
987+
retention_spec_or_period,
988+
retention_count,
989+
fs)
990+
if error_code_retention != 0:
991+
raise DashboardException(
992+
f'Failed to add retention policy for path {path}: {err_retention}'
993+
)
994+
if error_code != 0:
995+
raise DashboardException(
996+
f'Failed to create snapshot schedule for path {path}: {err}'
997+
)
998+
999+
return f'Snapshot schedule for path {path} created successfully'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<cd-modal [modalRef]="activeModal">
2+
<ng-container i18n="form title"
3+
class="modal-title">{{ action | titlecase }} {{ resource | upperFirst }}</ng-container>
4+
<ng-container class="modal-content"
5+
*cdFormLoading="loading">
6+
<form name="snapScheduleForm"
7+
#formDir="ngForm"
8+
[formGroup]="snapScheduleForm"
9+
novalidate>
10+
<div class="modal-body">
11+
<!-- Directory -->
12+
<div class="form-group row">
13+
<label class="cd-col-form-label required"
14+
for="directory"
15+
i18n>Directory
16+
</label>
17+
<div class="cd-col-form-input">
18+
<ng-template #loading>
19+
<i [ngClass]="[icons.spinner, icons.spin, 'mt-2', 'me-2']"></i>
20+
<span i18n>Loading directories</span>
21+
</ng-template>
22+
<select class="form-select"
23+
id="directory"
24+
name="directory"
25+
formControlName="directory"
26+
*ngIf="directories$ | async as directories; else loading">
27+
<option [ngValue]="null"
28+
i18n>--Select a directory--</option>
29+
<option *ngFor="let dir of directories"
30+
[value]="dir.path">{{ dir.path }}</option>
31+
</select>
32+
<span class="invalid-feedback"
33+
*ngIf="snapScheduleForm.showError('directory', formDir, 'required')"
34+
i18n>This field is required.</span>
35+
<span class="invalid-feedback"
36+
*ngIf="snapScheduleForm.showError('directory', formDir, 'notUnique')"
37+
i18n>A snapshot schedule for this path already exists.</span>
38+
</div>
39+
</div>
40+
<!--Start date -->
41+
<div class="form-group row">
42+
<label class="cd-col-form-label required"
43+
for="startDate"
44+
i18n>Start date
45+
</label>
46+
<div class="cd-col-form-input">
47+
<div class="input-group">
48+
<input class="form-control"
49+
placeholder="yyyy-mm-dd"
50+
name="startDate"
51+
id="startDate"
52+
formControlName="startDate"
53+
[minDate]="minDate"
54+
ngbDatepicker
55+
#d="ngbDatepicker"
56+
(click)="d.open()">
57+
<button type="button"
58+
class="btn btn-light"
59+
(click)="d.toggle()"
60+
title="Open">
61+
<i [ngClass]="icons.calendar"></i>
62+
</button>
63+
</div>
64+
<span class="invalid-feedback"
65+
*ngIf="snapScheduleForm.showError('startDate', formDir, 'required')"
66+
i18n>This field is required.</span>
67+
</div>
68+
</div>
69+
<!-- Start time -->
70+
<div class="form-group row">
71+
<label class="cd-col-form-label required"
72+
for="startTime"
73+
i18n>Start time
74+
<cd-helper>The time zone is assumed to be UTC.</cd-helper>
75+
</label>
76+
<div class="cd-col-form-input">
77+
<ngb-timepicker [spinners]="false"
78+
[seconds]="false"
79+
[meridian]="true"
80+
formControlName="startTime"
81+
id="startTime"
82+
name="startTime"></ngb-timepicker>
83+
<span class="invalid-feedback"
84+
*ngIf="snapScheduleForm.showError('startTime', formDir, 'required')"
85+
i18n>This field is required.</span>
86+
</div>
87+
</div>
88+
<!-- Repeat interval -->
89+
<div class="form-group row">
90+
<label class="cd-col-form-label required"
91+
for="repeatInterval"
92+
i18n>Schedule
93+
</label>
94+
<div class="cd-col-form-input">
95+
<div class="input-group">
96+
<input class="form-control"
97+
type="number"
98+
min="1"
99+
id="repeatInterval"
100+
name="repeatInterval"
101+
formControlName="repeatInterval">
102+
<select [ngClass]="['form-select', 'me-5']"
103+
id="repeatFrequency"
104+
name="repeatFrequency"
105+
formControlName="repeatFrequency"
106+
*ngIf="repeatFrequencies">
107+
<option *ngFor="let freq of repeatFrequencies"
108+
[value]="freq[1]"
109+
i18n>{{ freq[0] }}</option>
110+
</select>
111+
</div>
112+
<span class="invalid-feedback"
113+
*ngIf="snapScheduleForm.showError('repeatFrequency', formDir, 'notUnique')"
114+
i18n>This schedule already exists for the selected directory.</span>
115+
<span class="invalid-feedback"
116+
*ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'required')"
117+
i18n>This field is required.</span>
118+
<span class="invalid-feedback"
119+
*ngIf="snapScheduleForm.showError('repeatInterval', formDir, 'min')"
120+
i18n>Choose a value greater than 0.</span>
121+
</div>
122+
</div>
123+
<!-- Retention policies -->
124+
<ng-container formArrayName="retentionPolicies"
125+
*ngFor="let retentionPolicy of retentionPolicies.controls; index as i">
126+
<ng-container [formGroupName]="i">
127+
<div class="form-group row">
128+
<label [ngClass]="{'cd-col-form-label': true, 'visible': i == 0, 'invisible': i > 0}"
129+
for="retentionInterval"
130+
i18n>Retention policy
131+
</label>
132+
<div class="cd-col-form-input">
133+
<div class="input-group">
134+
<input class="form-control"
135+
type="number"
136+
min="1"
137+
id="retentionInterval"
138+
name="retentionInterval"
139+
formControlName="retentionInterval">
140+
<select class="form-select"
141+
id="retentionFrequency"
142+
name="retentionFrequency"
143+
formControlName="retentionFrequency"
144+
*ngIf="retentionFrequencies">
145+
<option *ngFor="let freq of retentionFrequencies"
146+
[value]="freq[1]"
147+
i18n>{{ freq[0] }}</option>
148+
</select>
149+
<button class="btn btn-light"
150+
type="button"
151+
(click)="removeRetentionPolicy(i)">
152+
<i [ngClass]="[icons.trash]"></i>
153+
</button>
154+
</div>
155+
<span class="invalid-feedback"
156+
*ngIf="snapScheduleForm.controls['retentionPolicies'].controls[i].invalid"
157+
i18n>This retention policy already exists for the selected directory.</span>
158+
</div>
159+
</div>
160+
</ng-container>
161+
</ng-container>
162+
<div class="d-flex flex-row align-content-center justify-content-end">
163+
<button class="btn btn-light"
164+
type="button"
165+
(click)="addRetentionPolicy()">
166+
<i [ngClass]="[icons.add, 'me-2']"></i>
167+
<span i18n>Add retention policy</span>
168+
</button>
169+
</div>
170+
</div>
171+
172+
<div class="modal-footer">
173+
<cd-form-button-panel (submitActionEvent)="submit()"
174+
[form]="snapScheduleForm"
175+
[submitText]="(action | titlecase) + ' ' + (resource | upperFirst)"></cd-form-button-panel>
176+
</div>
177+
</form>
178+
</ng-container>
179+
</cd-modal>

src/pybind/mgr/dashboard/frontend/src/app/ceph/cephfs/cephfs-snapshotschedule-form/cephfs-snapshotschedule-form.component.scss

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { HttpClientTestingModule } from '@angular/common/http/testing';
2+
import { ComponentFixture, TestBed } from '@angular/core/testing';
3+
4+
import { CephfsSnapshotscheduleFormComponent } from './cephfs-snapshotschedule-form.component';
5+
import {
6+
NgbActiveModal,
7+
NgbDatepickerModule,
8+
NgbTimepickerModule
9+
} from '@ng-bootstrap/ng-bootstrap';
10+
import { ToastrModule } from 'ngx-toastr';
11+
import { SharedModule } from '~/app/shared/shared.module';
12+
import { RouterTestingModule } from '@angular/router/testing';
13+
import { ReactiveFormsModule } from '@angular/forms';
14+
import { FormHelper, configureTestBed } from '~/testing/unit-test-helper';
15+
import { CephfsSnapshotScheduleService } from '~/app/shared/api/cephfs-snapshot-schedule.service';
16+
17+
describe('CephfsSnapshotscheduleFormComponent', () => {
18+
let component: CephfsSnapshotscheduleFormComponent;
19+
let fixture: ComponentFixture<CephfsSnapshotscheduleFormComponent>;
20+
let formHelper: FormHelper;
21+
let createSpy: jasmine.Spy;
22+
23+
configureTestBed({
24+
declarations: [CephfsSnapshotscheduleFormComponent],
25+
providers: [NgbActiveModal],
26+
imports: [
27+
SharedModule,
28+
ToastrModule.forRoot(),
29+
ReactiveFormsModule,
30+
HttpClientTestingModule,
31+
RouterTestingModule,
32+
NgbDatepickerModule,
33+
NgbTimepickerModule
34+
]
35+
});
36+
37+
beforeEach(() => {
38+
fixture = TestBed.createComponent(CephfsSnapshotscheduleFormComponent);
39+
component = fixture.componentInstance;
40+
component.fsName = 'test_fs';
41+
component.ngOnInit();
42+
formHelper = new FormHelper(component.snapScheduleForm);
43+
createSpy = spyOn(TestBed.inject(CephfsSnapshotScheduleService), 'create').and.stub();
44+
fixture.detectChanges();
45+
});
46+
47+
it('should create', () => {
48+
expect(component).toBeTruthy();
49+
});
50+
51+
it('should have a form open in modal', () => {
52+
const nativeEl = fixture.debugElement.nativeElement;
53+
expect(nativeEl.querySelector('cd-modal')).not.toBe(null);
54+
});
55+
56+
it('should submit the form', () => {
57+
const input = {
58+
directory: '/test',
59+
startDate: {
60+
year: 2023,
61+
month: 11,
62+
day: 14
63+
},
64+
startTime: {
65+
hour: 0,
66+
minute: 6,
67+
second: 22
68+
},
69+
repeatInterval: 4,
70+
repeatFrequency: 'h'
71+
};
72+
73+
formHelper.setMultipleValues(input);
74+
component.snapScheduleForm.get('directory').setValue('/test');
75+
component.submit();
76+
77+
expect(createSpy).toHaveBeenCalled();
78+
});
79+
});

0 commit comments

Comments
 (0)