1 Ext
.define('PVE.dc.BackupEdit', {
2 extend
: 'Proxmox.window.Edit',
3 alias
: ['widget.pveDcBackupEdit'],
5 defaultFocus
: undefined,
7 initComponent: function() {
10 me
.isCreate
= !me
.jobid
;
16 url
= '/api2/extjs/cluster/backup';
19 url
= '/api2/extjs/cluster/backup/' + me
.jobid
;
23 var vmidField
= Ext
.create('Ext.form.field.Hidden', {
27 // 'value' can be assigned a string or an array
28 var selModeField
= Ext
.create('Proxmox.form.KVComboBox', {
29 xtype
: 'proxmoxKVComboBox',
31 ['include', gettext('Include selected VMs')],
32 ['all', gettext('All')],
33 ['exclude', gettext('Exclude selected VMs')],
34 ['pool', gettext('Pool based')],
36 fieldLabel
: gettext('Selection mode'),
41 var sm
= Ext
.create('Ext.selection.CheckboxModel', {
44 selectionchange: function(model
, selected
) {
46 Ext
.Array
.each(selected
, function(record
) {
47 sel
.push(record
.data
.vmid
);
50 // to avoid endless recursion suspend the vmidField change
51 // event temporary as it calls us again
52 vmidField
.suspendEvent('change');
53 vmidField
.setValue(sel
);
54 vmidField
.resumeEvent('change');
59 var storagesel
= Ext
.create('PVE.form.StorageSelector', {
60 fieldLabel
: gettext('Storage'),
61 nodename
: 'localhost',
62 storageContent
: 'backup',
66 change: function(f
, v
) {
67 let store
= f
.getStore();
68 let rec
= store
.findRecord('storage', v
, 0, false, true, true);
69 let compressionSelector
= me
.down('pveCompressionSelector');
71 if (rec
&& rec
.data
&& rec
.data
.type
=== 'pbs') {
72 compressionSelector
.setValue('zstd');
73 compressionSelector
.setDisabled(true);
74 } else if (!compressionSelector
.getEditable()) {
75 compressionSelector
.setDisabled(false);
81 var store
= new Ext
.data
.Store({
82 model
: 'PVEResources',
89 var vmgrid
= Ext
.createWidget('grid', {
102 header
: gettext('Node'),
106 header
: gettext('Status'),
108 renderer: function(value
) {
110 return Proxmox
.Utils
.runningText
;
112 return Proxmox
.Utils
.stoppedText
;
117 header
: gettext('Name'),
122 header
: gettext('Type'),
128 var selectPoolMembers = function(poolid
) {
132 sm
.deselectAll(true);
143 var selPool
= Ext
.create('PVE.form.PoolSelector', {
144 fieldLabel
: gettext('Pool to backup'),
149 change: function(selpool
, newValue
, oldValue
) {
150 selectPoolMembers(newValue
);
155 var nodesel
= Ext
.create('PVE.form.NodeSelector', {
157 fieldLabel
: gettext('Node'),
161 emptyText
: '-- ' + gettext('All') + ' --',
163 change: function(f
, value
) {
164 storagesel
.setNodename(value
|| 'localhost');
165 var mode
= selModeField
.getValue();
167 store
.filterBy(function(rec
) {
168 return (!value
|| rec
.get('node') === value
);
170 if (mode
=== 'all') {
174 if (mode
=== 'pool') {
175 selectPoolMembers(selPool
.value
);
185 xtype
: 'pveDayOfWeekSelector',
187 fieldLabel
: gettext('Day of week'),
194 fieldLabel
: gettext('Start Time'),
208 fieldLabel
: gettext('Send email to'),
212 xtype
: 'pveEmailNotificationSelector',
213 fieldLabel
: gettext('Email notification'),
214 name
: 'mailnotification',
215 deleteEmpty
: !me
.isCreate
,
216 value
: me
.isCreate
? 'always' : '',
219 xtype
: 'pveCompressionSelector',
220 fieldLabel
: gettext('Compression'),
222 deleteEmpty
: !me
.isCreate
,
226 xtype
: 'pveBackupModeSelector',
227 fieldLabel
: gettext('Mode'),
232 xtype
: 'proxmoxcheckbox',
233 fieldLabel
: gettext('Enable'),
242 var ipanel
= Ext
.create('Proxmox.panel.InputPanel', {
243 onlineHelp
: 'chapter_vzdump',
246 onGetValues: function(values
) {
249 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'node' });
254 var selMode
= values
.selMode
;
255 delete values
.selMode
;
257 if (selMode
=== 'all') {
261 } else if (selMode
=== 'exclude') {
263 values
.exclude
= values
.vmid
;
265 } else if (selMode
=== 'pool') {
269 if (selMode
!== 'pool') {
276 var update_vmid_selection = function(list
, mode
) {
277 if (mode
!== 'all' && mode
!== 'pool') {
278 sm
.deselectAll(true);
280 Ext
.Array
.each(list
.split(','), function(vmid
) {
281 var rec
= store
.findRecord('vmid', vmid
, 0, false, true, true);
283 sm
.select(rec
, true);
290 vmidField
.on('change', function(f
, value
) {
291 var mode
= selModeField
.getValue();
292 update_vmid_selection(value
, mode
);
295 selModeField
.on('change', function(f
, value
, oldValue
) {
296 if (oldValue
=== 'pool') {
297 store
.removeFilter('poolFilter');
300 if (oldValue
=== 'all') {
301 sm
.deselectAll(true);
302 vmidField
.setValue('');
305 if (value
=== 'all') {
307 vmgrid
.setDisabled(true);
309 vmgrid
.setDisabled(false);
312 if (value
=== 'pool') {
313 vmgrid
.setDisabled(true);
314 vmidField
.setValue('');
315 selPool
.setVisible(true);
316 selPool
.allowBlank
= false;
317 selectPoolMembers(selPool
.value
);
319 selPool
.setVisible(false);
320 selPool
.allowBlank
= true;
322 var list
= vmidField
.getValue();
323 update_vmid_selection(list
, value
);
326 var reload = function() {
328 params
: { type
: 'vm' },
329 callback: function() {
330 var node
= nodesel
.getValue();
332 store
.filterBy(function(rec
) {
333 return (!node
|| node
.length
=== 0 || rec
.get('node') === node
);
335 var list
= vmidField
.getValue();
336 var mode
= selModeField
.getValue();
337 if (mode
=== 'all') {
339 } else if (mode
=== 'pool') {
340 selectPoolMembers(selPool
.value
);
342 update_vmid_selection(list
, mode
);
349 subject
: gettext("Backup Job"),
352 items
: [ipanel
, vmgrid
],
358 selModeField
.setValue('include');
361 success: function(response
, options
) {
362 var data
= response
.result
.data
;
364 data
.dow
= data
.dow
.split(',');
366 if (data
.all
|| data
.exclude
) {
368 data
.vmid
= data
.exclude
;
369 data
.selMode
= 'exclude';
372 data
.selMode
= 'all';
374 } else if (data
.pool
) {
375 data
.selMode
= 'pool';
376 data
.selPool
= data
.pool
;
378 data
.selMode
= 'include';
391 Ext
.define('PVE.dc.BackupDiskTree', {
392 extend
: 'Ext.tree.Panel',
393 alias
: 'widget.pveBackupDiskTree',
406 tooltip
: gettext('Expand All'),
408 callback: function(panel
) {
414 tooltip
: gettext('Collapse All'),
416 callback: function(panel
) {
425 text
: gettext('Guest Image'),
426 renderer: function(value
, meta
, record
) {
427 if (record
.data
.type
) {
430 if (record
.data
.name
) {
431 ret
+= " (" + record
.data
.name
+ ")";
436 // extJS needs unique IDs but we only want to show the
437 // volumes key from "vmid:key"
438 return value
.split(':')[1] + " - " + record
.data
.name
;
445 text
: gettext('Type'),
450 text
: gettext('Backup Job'),
451 renderer
: PVE
.Utils
.render_backup_status
,
452 dataIndex
: 'included',
459 var sm
= me
.getSelectionModel();
461 Proxmox
.Utils
.API2Request({
462 url
: "/cluster/backup/" + me
.jobid
+ "/included_volumes",
465 failure: function(response
, opts
) {
466 Proxmox
.Utils
.setErrorMask(me
, response
.htmlStatus
);
468 success: function(response
, opts
) {
470 me
.setRootNode(response
.result
.data
);
476 initComponent: function() {
480 throw "no job id specified";
483 var sm
= Ext
.create('Ext.selection.TreeModel', {});
487 fields
: ['id', 'type',
491 calculate: function(data
) {
492 var txt
= 'fa x-fa-tree fa-';
493 if (data
.leaf
&& !data
.type
) {
494 return txt
+ 'hdd-o';
495 } else if (data
.type
=== 'qemu') {
496 return txt
+ 'desktop';
497 } else if (data
.type
=== 'lxc') {
500 return txt
+ 'question-circle';
508 fieldLabel
: gettext('Search'),
510 emptyText
: 'Name, VMID, Type',
513 enableKeyEvents
: true,
516 keyup: function(field
) {
517 let searchValue
= field
.getValue();
518 searchValue
= searchValue
.toLowerCase();
520 me
.store
.clearFilter(true);
521 me
.store
.filterBy(function(record
) {
525 if (record
.data
.depth
== 0) {
527 } else if (record
.data
.depth
== 1) {
529 } else if (record
.data
.depth
== 2) {
530 data
= record
.parentNode
.data
;
533 Ext
.each(['name', 'id', 'type'], function(property
) {
534 if (data
[property
] === null) {
538 let v
= data
[property
].toString();
539 if (v
!== undefined) {
541 if (v
.includes(searchValue
)) {
562 Ext
.define('PVE.dc.BackupInfo', {
563 extend
: 'Proxmox.panel.InputPanel',
564 alias
: 'widget.pveBackupInfo',
571 fieldLabel
: gettext('Node'),
572 xtype
: 'displayfield',
573 renderer: function(value
) {
575 return '-- ' + gettext('All') + ' --';
583 fieldLabel
: gettext('Storage'),
584 xtype
: 'displayfield',
588 fieldLabel
: gettext('Day of week'),
589 xtype
: 'displayfield',
590 renderer
: PVE
.Utils
.render_backup_days_of_week
,
594 fieldLabel
: gettext('Start Time'),
595 xtype
: 'displayfield',
599 fieldLabel
: gettext('Selection mode'),
600 xtype
: 'displayfield',
604 fieldLabel
: gettext('Pool to backup'),
605 xtype
: 'displayfield',
611 fieldLabel
: gettext('Send email to'),
612 xtype
: 'displayfield',
615 name
: 'mailnotification',
616 fieldLabel
: gettext('Email notification'),
617 xtype
: 'displayfield',
618 renderer: function(value
) {
622 msg
= gettext('Always');
625 msg
= gettext('On failure only');
633 fieldLabel
: gettext('Compression'),
634 xtype
: 'displayfield',
638 fieldLabel
: gettext('Mode'),
639 xtype
: 'displayfield',
640 renderer: function(value
) {
644 msg
= gettext('Snapshot');
647 msg
= gettext('Suspend');
650 msg
= gettext('Stop');
658 fieldLabel
: gettext('Enabled'),
659 xtype
: 'displayfield',
660 renderer: function(value
) {
661 if (PVE
.Parser
.parseBoolean(value
.toString())) {
662 return gettext('Yes');
664 return gettext('No');
670 setValues: function(values
) {
673 Ext
.iterate(values
, function(fieldId
, val
) {
674 let field
= me
.query('[isFormField][name=' + fieldId
+ ']')[0];
680 // selection Mode depends on the presence/absence of several keys
681 let selModeField
= me
.query('[isFormField][name=selMode]')[0];
682 let selMode
= 'none';
684 selMode
= gettext('Include selected VMs');
687 selMode
= gettext('All');
689 if (values
.exclude
) {
690 selMode
= gettext('Exclude selected VMs');
693 selMode
= gettext('Pool based');
695 selModeField
.setValue(selMode
);
698 let poolField
= me
.query('[isFormField][name=pool]')[0];
699 poolField
.setVisible(0);
703 initComponent: function() {
707 throw "no data provided";
711 me
.setValues(me
.record
);
716 Ext
.define('PVE.dc.BackedGuests', {
717 extend
: 'Ext.grid.GridPanel',
718 alias
: 'widget.pveBackedGuests',
724 header
: gettext('Type'),
726 renderer
: PVE
.Utils
.render_resource_type
,
731 header
: gettext('VMID'),
737 header
: gettext('Name'),
744 initComponent: function() {
747 me
.store
.clearFilter(true);
751 stateId
: 'grid-dc-backed-guests',
754 gettext('Search') + ':', ' ',
758 emptyText
: 'Name, VMID, Type',
759 enableKeyEvents
: true,
762 keyup: function(field
) {
763 let searchValue
= field
.getValue();
764 searchValue
= searchValue
.toLowerCase();
766 me
.store
.clearFilter(true);
767 me
.store
.filterBy(function(record
) {
770 Ext
.each(['name', 'vmid', 'type'], function(property
) {
771 if (record
.data
[property
] == null) {
775 let v
= record
.data
[property
].toString();
776 if (v
!== undefined) {
778 if (v
.includes(searchValue
)) {
799 Ext
.define('PVE.dc.BackupView', {
800 extend
: 'Ext.grid.GridPanel',
802 alias
: ['widget.pveDcBackupView'],
804 onlineHelp
: 'chapter_vzdump',
806 allText
: '-- ' + gettext('All') + ' --',
808 initComponent: function() {
811 var store
= new Ext
.data
.Store({
812 model
: 'pve-cluster-backup',
815 url
: "/api2/json/cluster/backup",
819 var not_backed_store
= new Ext
.data
.Store({
823 url
: 'api2/json/cluster/backupinfo/not_backed_up',
827 var reload = function() {
829 not_backed_store
.load({
830 callback: function(records
, operation
, success
) {
831 if (records
.length
) {
832 not_backed_warning
.setVisible(true);
833 not_backed_btn
.setVisible(true);
835 not_backed_warning
.setVisible(false);
836 not_backed_btn
.setVisible(false);
842 var sm
= Ext
.create('Ext.selection.RowModel', {});
844 var run_editor = function() {
845 var rec
= sm
.getSelection()[0];
850 var win
= Ext
.create('PVE.dc.BackupEdit', {
853 win
.on('destroy', reload
);
857 var run_detail = function() {
859 let record
= sm
.getSelection()[0];
863 let infoview
= Ext
.create('PVE.dc.BackupInfo', {
868 let disktree
= Ext
.create('PVE.dc.BackupDiskTree', {
869 title
: gettext('Included disks'),
871 jobid
: record
.data
.id
,
874 Ext
.create('Ext.window.Window', {
879 stateId
: 'backup-detail-view',
882 title
: gettext('Backup Details'),
891 items
: [infoview
, disktree
],
896 var run_backup_now = function(job
) {
897 job
= Ext
.clone(job
);
899 let jobNode
= job
.node
;
900 // Remove properties related to scheduling
902 delete job
.starttime
;
906 job
.all
= job
.all
=== true ? 1 : 0;
908 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
909 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
912 if (jobNode
!== undefined) {
913 if (!nodes
.includes(jobNode
)) {
914 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
919 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
920 if (unkownNodes
.length
> 0)
921 errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));
923 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
926 title
: gettext('Please wait...'),
929 progressText
: '0/' + jobTotalCount
,
932 let postRequest = function() {
934 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
936 if (jobsStarted
== jobTotalCount
) {
938 if (errors
.length
> 0) {
939 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
944 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
945 url
: '/nodes/' + node
+ '/vzdump',
948 failure: function(response
, opts
) {
949 errors
.push(node
+ ': ' + response
.htmlStatus
);
952 success
: postRequest
,
956 var run_show_not_backed = function() {
958 var backedinfo
= Ext
.create('PVE.dc.BackedGuests', {
961 store
: not_backed_store
,
964 var win
= Ext
.create('Ext.window.Window', {
970 title
: gettext('Guests without backup job'),
984 var edit_btn
= new Proxmox
.button
.Button({
985 text
: gettext('Edit'),
991 var run_btn
= new Proxmox
.button
.Button({
992 text
: gettext('Run now'),
995 handler: function() {
996 var rec
= sm
.getSelection()[0];
1002 title
: gettext('Confirm'),
1003 icon
: Ext
.Msg
.QUESTION
,
1004 msg
: gettext('Start the selected backup job now?'),
1005 buttons
: Ext
.Msg
.YESNO
,
1006 callback: function(btn
) {
1007 if (btn
!== 'yes') {
1010 run_backup_now(rec
.data
);
1016 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
1018 baseurl
: '/cluster/backup',
1019 callback: function() {
1024 var detail_btn
= new Proxmox
.button
.Button({
1025 text
: gettext('Job Detail'),
1027 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
1029 handler
: run_detail
,
1032 var not_backed_warning
= Ext
.create('Ext.toolbar.TextItem', {
1033 html
: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
1037 var not_backed_btn
= new Proxmox
.button
.Button({
1038 text
: gettext('Show'),
1040 handler
: run_show_not_backed
,
1043 Proxmox
.Utils
.monStoreErrors(me
, store
);
1049 stateId
: 'grid-dc-backup',
1055 text
: gettext('Add'),
1056 handler: function() {
1057 var win
= Ext
.create('PVE.dc.BackupEdit', {});
1058 win
.on('destroy', reload
);
1074 header
: gettext('Enabled'),
1076 dataIndex
: 'enabled',
1077 xtype
: 'checkcolumn',
1080 disabledCls
: 'x-item-enabled',
1081 stopSelection
: false,
1084 header
: gettext('Node'),
1088 renderer: function(value
) {
1096 header
: gettext('Day of week'),
1100 renderer
: PVE
.Utils
.render_backup_days_of_week
,
1103 header
: gettext('Start Time'),
1106 dataIndex
: 'starttime',
1109 header
: gettext('Storage'),
1112 dataIndex
: 'storage',
1115 header
: gettext('Selection'),
1119 renderer
: PVE
.Utils
.render_backup_selection
,
1124 itemdblclick
: run_editor
,
1131 Ext
.define('pve-cluster-backup', {
1132 extend
: 'Ext.data.Model',
1134 'id', 'starttime', 'dow',
1135 'storage', 'node', 'vmid', 'exclude',
1136 'mailto', 'pool', 'compress', 'mode',
1137 { name
: 'enabled', type
: 'boolean' },
1138 { name
: 'all', type
: 'boolean' },