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