]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
ui: backup/run now: avoid passing "next-run" parameter to backend
[pve-manager.git] / www / manager6 / dc / Backup.js
1 Ext.define('PVE.dc.BackupEdit', {
2 extend: 'Proxmox.window.Edit',
3 alias: ['widget.pveDcBackupEdit'],
4
5 defaultFocus: undefined,
6
7 initComponent: function() {
8 let me = this;
9
10 me.isCreate = !me.jobid;
11
12 let url, method;
13 if (me.isCreate) {
14 url = '/api2/extjs/cluster/backup';
15 method = 'POST';
16 } else {
17 url = '/api2/extjs/cluster/backup/' + me.jobid;
18 method = 'PUT';
19 }
20
21 let vmidField = Ext.create('Ext.form.field.Hidden', {
22 name: 'vmid',
23 });
24
25 // 'value' can be assigned a string or an array
26 let selModeField = Ext.create('Proxmox.form.KVComboBox', {
27 xtype: 'proxmoxKVComboBox',
28 comboItems: [
29 ['include', gettext('Include selected VMs')],
30 ['all', gettext('All')],
31 ['exclude', gettext('Exclude selected VMs')],
32 ['pool', gettext('Pool based')],
33 ],
34 fieldLabel: gettext('Selection mode'),
35 name: 'selMode',
36 value: '',
37 });
38
39 let sm = Ext.create('Ext.selection.CheckboxModel', {
40 mode: 'SIMPLE',
41 listeners: {
42 selectionchange: function(model, selected) {
43 let sel = selected.map(record => record.data.vmid);
44 // to avoid endless recursion suspend the vmidField change
45 // event temporary as it calls us again
46 vmidField.suspendEvent('change');
47 vmidField.setValue(sel);
48 vmidField.resumeEvent('change');
49 },
50 },
51 });
52
53 let storagesel = Ext.create('PVE.form.StorageSelector', {
54 fieldLabel: gettext('Storage'),
55 clusterView: true,
56 storageContent: 'backup',
57 allowBlank: false,
58 name: 'storage',
59 listeners: {
60 change: function(f, v) {
61 let store = f.getStore();
62 let rec = store.findRecord('storage', v, 0, false, true, true);
63 let compressionSelector = me.down('pveCompressionSelector');
64
65 if (rec && rec.data && rec.data.type === 'pbs') {
66 compressionSelector.setValue('zstd');
67 compressionSelector.setDisabled(true);
68 } else if (!compressionSelector.getEditable()) {
69 compressionSelector.setDisabled(false);
70 }
71 },
72 },
73 });
74
75 let store = new Ext.data.Store({
76 model: 'PVEResources',
77 sorters: {
78 property: 'vmid',
79 direction: 'ASC',
80 },
81 });
82
83 let vmgrid = Ext.createWidget('grid', {
84 store: store,
85 border: true,
86 height: 300,
87 selModel: sm,
88 disabled: true,
89 columns: [
90 {
91 header: 'ID',
92 dataIndex: 'vmid',
93 width: 60,
94 },
95 {
96 header: gettext('Node'),
97 dataIndex: 'node',
98 },
99 {
100 header: gettext('Status'),
101 dataIndex: 'uptime',
102 renderer: function(value) {
103 if (value) {
104 return Proxmox.Utils.runningText;
105 } else {
106 return Proxmox.Utils.stoppedText;
107 }
108 },
109 },
110 {
111 header: gettext('Name'),
112 dataIndex: 'name',
113 flex: 1,
114 },
115 {
116 header: gettext('Type'),
117 dataIndex: 'type',
118 },
119 ],
120 });
121
122 let selectPoolMembers = function(poolid) {
123 if (!poolid) {
124 return;
125 }
126 sm.deselectAll(true);
127 store.filter([
128 {
129 id: 'poolFilter',
130 property: 'pool',
131 value: poolid,
132 },
133 ]);
134 sm.selectAll(true);
135 };
136
137 let selPool = Ext.create('PVE.form.PoolSelector', {
138 fieldLabel: gettext('Pool to backup'),
139 hidden: true,
140 allowBlank: true,
141 name: 'pool',
142 listeners: {
143 change: function(selpool, newValue, oldValue) {
144 selectPoolMembers(newValue);
145 },
146 },
147 });
148
149 let nodesel = Ext.create('PVE.form.NodeSelector', {
150 name: 'node',
151 fieldLabel: gettext('Node'),
152 allowBlank: true,
153 editable: true,
154 autoSelect: false,
155 emptyText: '-- ' + gettext('All') + ' --',
156 listeners: {
157 change: function(f, value) {
158 storagesel.setNodename(value);
159 let mode = selModeField.getValue();
160 store.clearFilter();
161 store.filterBy(function(rec) {
162 return !value || rec.get('node') === value;
163 });
164 if (mode === 'all') {
165 sm.selectAll(true);
166 }
167 if (mode === 'pool') {
168 selectPoolMembers(selPool.value);
169 }
170 },
171 },
172 });
173
174 let column1 = [
175 nodesel,
176 storagesel,
177 {
178 xtype: 'pveCalendarEvent',
179 fieldLabel: gettext('Schedule'),
180 allowBlank: false,
181 name: 'schedule',
182 },
183 selModeField,
184 selPool,
185 ];
186
187 let column2 = [
188 {
189 xtype: 'textfield',
190 fieldLabel: gettext('Send email to'),
191 name: 'mailto',
192 },
193 {
194 xtype: 'pveEmailNotificationSelector',
195 fieldLabel: gettext('Email'),
196 name: 'mailnotification',
197 deleteEmpty: !me.isCreate,
198 value: me.isCreate ? 'always' : '',
199 },
200 {
201 xtype: 'pveCompressionSelector',
202 fieldLabel: gettext('Compression'),
203 name: 'compress',
204 deleteEmpty: !me.isCreate,
205 value: 'zstd',
206 },
207 {
208 xtype: 'pveBackupModeSelector',
209 fieldLabel: gettext('Mode'),
210 value: 'snapshot',
211 name: 'mode',
212 },
213 {
214 xtype: 'proxmoxcheckbox',
215 fieldLabel: gettext('Enable'),
216 name: 'enabled',
217 uncheckedValue: 0,
218 defaultValue: 1,
219 checked: true,
220 },
221 vmidField,
222 ];
223
224 let ipanel = Ext.create('Proxmox.panel.InputPanel', {
225 onlineHelp: 'chapter_vzdump',
226 column1: column1,
227 column2: column2,
228 columnB: [
229 {
230 xtype: 'proxmoxtextfield',
231 name: 'comment',
232 fieldLabel: gettext('Comment'),
233 deleteEmpty: !me.isCreate,
234 },
235 ],
236 onGetValues: function(values) {
237 if (!values.node) {
238 if (!me.isCreate) {
239 Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
240 }
241 delete values.node;
242 }
243
244 if (!values.id && me.isCreate) {
245 values.id = 'backup-' + Ext.data.identifier.Uuid.Global.generate().slice(0, 13);
246 }
247
248 let selMode = values.selMode;
249 delete values.selMode;
250
251 if (selMode === 'all') {
252 values.all = 1;
253 values.exclude = '';
254 delete values.vmid;
255 } else if (selMode === 'exclude') {
256 values.all = 1;
257 values.exclude = values.vmid;
258 delete values.vmid;
259 } else if (selMode === 'pool') {
260 delete values.vmid;
261 }
262
263 if (selMode !== 'pool') {
264 delete values.pool;
265 }
266 return values;
267 },
268 });
269
270 let update_vmid_selection = function(list, mode) {
271 if (mode !== 'all' && mode !== 'pool') {
272 sm.deselectAll(true);
273 if (list) {
274 Ext.Array.each(list.split(','), function(vmid) {
275 var rec = store.findRecord('vmid', vmid, 0, false, true, true);
276 if (rec) {
277 sm.select(rec, true);
278 }
279 });
280 }
281 }
282 };
283
284 vmidField.on('change', function(f, value) {
285 let mode = selModeField.getValue();
286 update_vmid_selection(value, mode);
287 });
288
289 selModeField.on('change', function(f, value, oldValue) {
290 if (oldValue === 'pool') {
291 store.removeFilter('poolFilter');
292 }
293
294 if (oldValue === 'all') {
295 sm.deselectAll(true);
296 vmidField.setValue('');
297 }
298
299 if (value === 'all') {
300 sm.selectAll(true);
301 vmgrid.setDisabled(true);
302 } else {
303 vmgrid.setDisabled(false);
304 }
305
306 if (value === 'pool') {
307 vmgrid.setDisabled(true);
308 vmidField.setValue('');
309 selPool.setVisible(true);
310 selPool.allowBlank = false;
311 selectPoolMembers(selPool.value);
312 } else {
313 selPool.setVisible(false);
314 selPool.allowBlank = true;
315 }
316 let list = vmidField.getValue();
317 update_vmid_selection(list, value);
318 });
319
320 let reload = function() {
321 store.load({
322 params: {
323 type: 'vm',
324 },
325 callback: function() {
326 let node = nodesel.getValue();
327 store.clearFilter();
328 store.filterBy(rec => !node || node.length === 0 || rec.get('node') === node);
329 let list = vmidField.getValue();
330 let mode = selModeField.getValue();
331 if (mode === 'all') {
332 sm.selectAll(true);
333 } else if (mode === 'pool') {
334 selectPoolMembers(selPool.value);
335 } else {
336 update_vmid_selection(list, mode);
337 }
338 },
339 });
340 };
341
342 Ext.applyIf(me, {
343 subject: gettext("Backup Job"),
344 url: url,
345 method: method,
346 bodyPadding: 0,
347 items: [
348 {
349 xtype: 'tabpanel',
350 region: 'center',
351 layout: 'fit',
352 bodyPadding: 10,
353 items: [
354 {
355 xtype: 'container',
356 title: gettext('General'),
357 region: 'center',
358 layout: {
359 type: 'vbox',
360 align: 'stretch',
361 },
362 items: [
363 ipanel,
364 vmgrid,
365 ],
366 },
367 {
368 xtype: 'pveBackupJobPrunePanel',
369 title: gettext('Retention'),
370 isCreate: me.isCreate,
371 keepAllDefaultForCreate: false,
372 showPBSHint: false,
373 fallbackHintHtml: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
374 },
375 ],
376 },
377 ],
378
379 });
380
381 me.callParent();
382
383 if (me.isCreate) {
384 selModeField.setValue('include');
385 } else {
386 me.load({
387 success: function(response, options) {
388 let data = response.result.data;
389
390 data.dow = (data.dow || '').split(',');
391
392 if (data.all || data.exclude) {
393 if (data.exclude) {
394 data.vmid = data.exclude;
395 data.selMode = 'exclude';
396 } else {
397 data.vmid = '';
398 data.selMode = 'all';
399 }
400 } else if (data.pool) {
401 data.selMode = 'pool';
402 data.selPool = data.pool;
403 } else {
404 data.selMode = 'include';
405 }
406
407 if (data['prune-backups']) {
408 Object.assign(data, data['prune-backups']);
409 delete data['prune-backups'];
410 } else if (data.maxfiles !== undefined) {
411 if (data.maxfiles > 0) {
412 data['keep-last'] = data.maxfiles;
413 } else {
414 data['keep-all'] = 1;
415 }
416 delete data.maxfiles;
417 }
418
419 me.setValues(data);
420 },
421 });
422 }
423
424 reload();
425 },
426 });
427
428 Ext.define('PVE.dc.BackupView', {
429 extend: 'Ext.grid.GridPanel',
430
431 alias: ['widget.pveDcBackupView'],
432
433 onlineHelp: 'chapter_vzdump',
434
435 allText: '-- ' + gettext('All') + ' --',
436
437 initComponent: function() {
438 let me = this;
439
440 let store = new Ext.data.Store({
441 model: 'pve-cluster-backup',
442 proxy: {
443 type: 'proxmox',
444 url: "/api2/json/cluster/backup",
445 },
446 });
447
448 let not_backed_store = new Ext.data.Store({
449 sorters: 'vmid',
450 proxy: {
451 type: 'proxmox',
452 url: 'api2/json/cluster/backup-info/not-backed-up',
453 },
454 });
455
456 let noBackupJobWarning, noBackupJobInfoButton;
457 let reload = function() {
458 store.load();
459 not_backed_store.load({
460 callback: function(records, operation, success) {
461 noBackupJobWarning.setVisible(records.length > 0);
462 noBackupJobInfoButton.setVisible(records.length > 0);
463 },
464 });
465 };
466
467 let sm = Ext.create('Ext.selection.RowModel', {});
468
469 let run_editor = function() {
470 let rec = sm.getSelection()[0];
471 if (!rec) {
472 return;
473 }
474
475 let win = Ext.create('PVE.dc.BackupEdit', {
476 jobid: rec.data.id,
477 });
478 win.on('destroy', reload);
479 win.show();
480 };
481
482 let run_detail = function() {
483 let record = sm.getSelection()[0];
484 if (!record) {
485 return;
486 }
487 Ext.create('Ext.window.Window', {
488 modal: true,
489 width: 800,
490 height: 700,
491 resizable: true,
492 layout: 'fit',
493 title: gettext('Backup Details'),
494 items: [
495 {
496 xtype: 'panel',
497 region: 'center',
498 layout: {
499 type: 'vbox',
500 align: 'stretch',
501 },
502 items: [
503 {
504 xtype: 'pveBackupInfo',
505 flex: 0,
506 layout: 'fit',
507 record: record.data,
508 },
509 {
510 xtype: 'pveBackupDiskTree',
511 title: gettext('Included disks'),
512 flex: 1,
513 jobid: record.data.id,
514 },
515 ],
516 },
517 ],
518 }).show();
519 };
520
521 let run_backup_now = function(job) {
522 job = Ext.clone(job);
523
524 let jobNode = job.node;
525 // Remove properties related to scheduling
526 delete job.enabled;
527 delete job.starttime;
528 delete job.dow;
529 delete job.id;
530 delete job.schedule;
531 delete job.type;
532 delete job.node;
533 delete job.comment;
534 delete job['next-run'];
535 job.all = job.all === true ? 1 : 0;
536
537 if (job['prune-backups']) {
538 job['prune-backups'] = PVE.Parser.printPropertyString(job['prune-backups']);
539 }
540
541 let allNodes = PVE.data.ResourceStore.getNodes();
542 let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
543 let errors = [];
544
545 if (jobNode !== undefined) {
546 if (!nodes.includes(jobNode)) {
547 Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
548 return;
549 }
550 nodes = [jobNode];
551 } else {
552 let unkownNodes = allNodes.filter(node => node.status !== 'online');
553 if (unkownNodes.length > 0) {errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));}
554 }
555 let jobTotalCount = nodes.length, jobsStarted = 0;
556
557 Ext.Msg.show({
558 title: gettext('Please wait...'),
559 closable: false,
560 progress: true,
561 progressText: '0/' + jobTotalCount,
562 });
563
564 let postRequest = function() {
565 jobsStarted++;
566 Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
567
568 if (jobsStarted === jobTotalCount) {
569 Ext.Msg.hide();
570 if (errors.length > 0) {
571 Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
572 }
573 }
574 };
575
576 nodes.forEach(node => Proxmox.Utils.API2Request({
577 url: '/nodes/' + node + '/vzdump',
578 method: 'POST',
579 params: job,
580 failure: function(response, opts) {
581 errors.push(node + ': ' + response.htmlStatus);
582 postRequest();
583 },
584 success: postRequest,
585 }));
586 };
587
588 let run_show_not_backed = function() {
589 Ext.create('Ext.window.Window', {
590 modal: true,
591 width: 600,
592 height: 500,
593 resizable: true,
594 layout: 'fit',
595 title: gettext('Guests without backup job'),
596 items: [
597 {
598 xtype: 'panel',
599 region: 'center',
600 layout: {
601 type: 'vbox',
602 align: 'stretch',
603 },
604 items: [
605 {
606 xtype: 'pveBackedGuests',
607 flex: 1,
608 layout: 'fit',
609 store: not_backed_store,
610 },
611 ],
612 },
613 ],
614 }).show();
615 };
616
617 var edit_btn = new Proxmox.button.Button({
618 text: gettext('Edit'),
619 disabled: true,
620 selModel: sm,
621 handler: run_editor,
622 });
623
624 var run_btn = new Proxmox.button.Button({
625 text: gettext('Run now'),
626 disabled: true,
627 selModel: sm,
628 handler: function() {
629 var rec = sm.getSelection()[0];
630 if (!rec) {
631 return;
632 }
633
634 Ext.Msg.show({
635 title: gettext('Confirm'),
636 icon: Ext.Msg.QUESTION,
637 msg: gettext('Start the selected backup job now?'),
638 buttons: Ext.Msg.YESNO,
639 callback: function(btn) {
640 if (btn !== 'yes') {
641 return;
642 }
643 run_backup_now(rec.data);
644 },
645 });
646 },
647 });
648
649 var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
650 selModel: sm,
651 baseurl: '/cluster/backup',
652 callback: function() {
653 reload();
654 },
655 });
656
657 var detail_btn = new Proxmox.button.Button({
658 text: gettext('Job Detail'),
659 disabled: true,
660 tooltip: gettext('Show job details and which guests and volumes are affected by the backup job'),
661 selModel: sm,
662 handler: run_detail,
663 });
664
665 noBackupJobWarning = Ext.create('Ext.toolbar.TextItem', {
666 html: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
667 hidden: true,
668 });
669
670 noBackupJobInfoButton = new Proxmox.button.Button({
671 text: gettext('Show'),
672 hidden: true,
673 handler: run_show_not_backed,
674 });
675
676 Proxmox.Utils.monStoreErrors(me, store);
677
678 Ext.apply(me, {
679 store: store,
680 selModel: sm,
681 stateful: true,
682 stateId: 'grid-dc-backup',
683 viewConfig: {
684 trackOver: false,
685 },
686 tbar: [
687 {
688 text: gettext('Add'),
689 handler: function() {
690 var win = Ext.create('PVE.dc.BackupEdit', {});
691 win.on('destroy', reload);
692 win.show();
693 },
694 },
695 '-',
696 remove_btn,
697 edit_btn,
698 detail_btn,
699 '-',
700 run_btn,
701 '->',
702 noBackupJobWarning,
703 noBackupJobInfoButton,
704 '-',
705 {
706 xtype: 'proxmoxButton',
707 selModel: null,
708 text: gettext('Schedule Simulator'),
709 handler: () => {
710 let record = sm.getSelection()[0];
711 let schedule;
712 if (record) {
713 schedule = record.data.schedule;
714 }
715 Ext.create('PVE.window.ScheduleSimulator', {
716 schedule,
717 }).show();
718 },
719 },
720 ],
721 columns: [
722 {
723 header: gettext('Enabled'),
724 width: 80,
725 dataIndex: 'enabled',
726 xtype: 'checkcolumn',
727 sortable: true,
728 disabled: true,
729 disabledCls: 'x-item-enabled',
730 stopSelection: false,
731 },
732 {
733 header: gettext('ID'),
734 dataIndex: 'id',
735 hidden: true,
736 },
737 {
738 header: gettext('Node'),
739 width: 100,
740 sortable: true,
741 dataIndex: 'node',
742 renderer: function(value) {
743 if (value) {
744 return value;
745 }
746 return me.allText;
747 },
748 },
749 {
750 header: gettext('Schedule'),
751 width: 150,
752 dataIndex: 'schedule',
753 },
754 {
755 text: gettext('Next Run'),
756 dataIndex: 'next-run',
757 width: 150,
758 renderer: PVE.Utils.render_next_event,
759 },
760 {
761 header: gettext('Storage'),
762 width: 100,
763 sortable: true,
764 dataIndex: 'storage',
765 },
766 {
767 header: gettext('Comment'),
768 dataIndex: 'comment',
769 renderer: Ext.htmlEncode,
770 sorter: (a, b) => (a.data.comment || '').localeCompare(b.data.comment || ''),
771 flex: 1,
772 },
773 {
774 header: gettext('Retention'),
775 dataIndex: 'prune-backups',
776 renderer: v => v ? PVE.Parser.printPropertyString(v) : gettext('Fallback from storage config'),
777 flex: 2,
778 },
779 {
780 header: gettext('Selection'),
781 flex: 4,
782 sortable: false,
783 dataIndex: 'vmid',
784 renderer: PVE.Utils.render_backup_selection,
785 },
786 ],
787 listeners: {
788 activate: reload,
789 itemdblclick: run_editor,
790 },
791 });
792
793 me.callParent();
794 },
795 }, function() {
796 Ext.define('pve-cluster-backup', {
797 extend: 'Ext.data.Model',
798 fields: [
799 'id',
800 'compress',
801 'dow',
802 'exclude',
803 'mailto',
804 'mode',
805 'node',
806 'pool',
807 'prune-backups',
808 'starttime',
809 'storage',
810 'vmid',
811 { name: 'enabled', type: 'boolean' },
812 { name: 'all', type: 'boolean' },
813 ],
814 });
815 });