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