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