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