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
;
14 url
= '/api2/extjs/cluster/backup';
17 url
= '/api2/extjs/cluster/backup/' + me
.jobid
;
21 let vmidField
= Ext
.create('Ext.form.field.Hidden', {
25 // 'value' can be assigned a string or an array
26 let selModeField
= Ext
.create('Proxmox.form.KVComboBox', {
27 xtype
: 'proxmoxKVComboBox',
29 ['include', gettext('Include selected VMs')],
30 ['all', gettext('All')],
31 ['exclude', gettext('Exclude selected VMs')],
32 ['pool', gettext('Pool based')],
34 fieldLabel
: gettext('Selection mode'),
39 let sm
= Ext
.create('Ext.selection.CheckboxModel', {
42 selectionchange: function(model
, selected
) {
44 Ext
.Array
.each(selected
, function(record
) {
45 sel
.push(record
.data
.vmid
);
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');
57 let storagesel
= Ext
.create('PVE.form.StorageSelector', {
58 fieldLabel
: gettext('Storage'),
59 nodename
: 'localhost',
60 storageContent
: 'backup',
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');
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);
79 let store
= new Ext
.data
.Store({
80 model
: 'PVEResources',
87 let vmgrid
= Ext
.createWidget('grid', {
100 header
: gettext('Node'),
104 header
: gettext('Status'),
106 renderer: function(value
) {
108 return Proxmox
.Utils
.runningText
;
110 return Proxmox
.Utils
.stoppedText
;
115 header
: gettext('Name'),
120 header
: gettext('Type'),
126 let selectPoolMembers = function(poolid
) {
130 sm
.deselectAll(true);
141 let selPool
= Ext
.create('PVE.form.PoolSelector', {
142 fieldLabel
: gettext('Pool to backup'),
147 change: function(selpool
, newValue
, oldValue
) {
148 selectPoolMembers(newValue
);
153 let nodesel
= Ext
.create('PVE.form.NodeSelector', {
155 fieldLabel
: gettext('Node'),
159 emptyText
: '-- ' + gettext('All') + ' --',
161 change: function(f
, value
) {
162 storagesel
.setNodename(value
|| 'localhost');
163 let mode
= selModeField
.getValue();
165 store
.filterBy(function(rec
) {
166 return !value
|| rec
.get('node') === value
;
168 if (mode
=== 'all') {
171 if (mode
=== 'pool') {
172 selectPoolMembers(selPool
.value
);
182 xtype
: 'pveDayOfWeekSelector',
184 fieldLabel
: gettext('Day of week'),
191 fieldLabel
: gettext('Start Time'),
205 fieldLabel
: gettext('Send email to'),
209 xtype
: 'pveEmailNotificationSelector',
210 fieldLabel
: gettext('Email notification'),
211 name
: 'mailnotification',
212 deleteEmpty
: !me
.isCreate
,
213 value
: me
.isCreate
? 'always' : '',
216 xtype
: 'pveCompressionSelector',
217 fieldLabel
: gettext('Compression'),
219 deleteEmpty
: !me
.isCreate
,
223 xtype
: 'pveBackupModeSelector',
224 fieldLabel
: gettext('Mode'),
229 xtype
: 'proxmoxcheckbox',
230 fieldLabel
: gettext('Enable'),
239 let ipanel
= Ext
.create('Proxmox.panel.InputPanel', {
240 onlineHelp
: 'chapter_vzdump',
243 onGetValues: function(values
) {
246 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'node' });
251 let selMode
= values
.selMode
;
252 delete values
.selMode
;
254 if (selMode
=== 'all') {
258 } else if (selMode
=== 'exclude') {
260 values
.exclude
= values
.vmid
;
262 } else if (selMode
=== 'pool') {
266 if (selMode
!== 'pool') {
273 let update_vmid_selection = function(list
, mode
) {
274 if (mode
!== 'all' && mode
!== 'pool') {
275 sm
.deselectAll(true);
277 Ext
.Array
.each(list
.split(','), function(vmid
) {
278 var rec
= store
.findRecord('vmid', vmid
, 0, false, true, true);
280 sm
.select(rec
, true);
287 vmidField
.on('change', function(f
, value
) {
288 let mode
= selModeField
.getValue();
289 update_vmid_selection(value
, mode
);
292 selModeField
.on('change', function(f
, value
, oldValue
) {
293 if (oldValue
=== 'pool') {
294 store
.removeFilter('poolFilter');
297 if (oldValue
=== 'all') {
298 sm
.deselectAll(true);
299 vmidField
.setValue('');
302 if (value
=== 'all') {
304 vmgrid
.setDisabled(true);
306 vmgrid
.setDisabled(false);
309 if (value
=== 'pool') {
310 vmgrid
.setDisabled(true);
311 vmidField
.setValue('');
312 selPool
.setVisible(true);
313 selPool
.allowBlank
= false;
314 selectPoolMembers(selPool
.value
);
316 selPool
.setVisible(false);
317 selPool
.allowBlank
= true;
319 let list
= vmidField
.getValue();
320 update_vmid_selection(list
, value
);
323 let reload = function() {
328 callback: function() {
329 let node
= nodesel
.getValue();
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') {
336 } else if (mode
=== 'pool') {
337 selectPoolMembers(selPool
.value
);
339 update_vmid_selection(list
, mode
);
346 subject
: gettext("Backup Job"),
359 title
: gettext('General'),
371 xtype
: 'pveBackupJobPrunePanel',
372 title
: gettext('Retention'),
373 isCreate
: me
.isCreate
,
374 keepAllDefaultForCreate
: false,
376 fallbackHintHtml
: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
387 selModeField
.setValue('include');
390 success: function(response
, options
) {
391 let data
= response
.result
.data
;
393 data
.dow
= data
.dow
.split(',');
395 if (data
.all
|| data
.exclude
) {
397 data
.vmid
= data
.exclude
;
398 data
.selMode
= 'exclude';
401 data
.selMode
= 'all';
403 } else if (data
.pool
) {
404 data
.selMode
= 'pool';
405 data
.selPool
= data
.pool
;
407 data
.selMode
= 'include';
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
;
417 data
['keep-all'] = 1;
419 delete data
.maxfiles
;
432 Ext
.define('PVE.dc.BackupDiskTree', {
433 extend
: 'Ext.tree.Panel',
434 alias
: 'widget.pveBackupDiskTree',
447 tooltip
: gettext('Expand All'),
448 callback
: panel
=> panel
.expandAll(),
452 tooltip
: gettext('Collapse All'),
453 callback
: panel
=> panel
.collapseAll(),
460 text
: gettext('Guest Image'),
461 renderer: function(value
, meta
, record
) {
462 if (record
.data
.type
) {
465 if (record
.data
.name
) {
466 ret
+= " (" + record
.data
.name
+ ")";
470 // extJS needs unique IDs but we only want to show the volumes key from "vmid:key"
471 return value
.split(':')[1] + " - " + record
.data
.name
;
478 text
: gettext('Type'),
483 text
: gettext('Backup Job'),
484 renderer
: PVE
.Utils
.render_backup_status
,
485 dataIndex
: 'included',
492 let sm
= me
.getSelectionModel();
494 Proxmox
.Utils
.API2Request({
495 url
: `/cluster/backup/${me.jobid}/included_volumes`,
498 failure: function(response
, opts
) {
499 Proxmox
.Utils
.setErrorMask(me
, response
.htmlStatus
);
501 success: function(response
, opts
) {
503 me
.setRootNode(response
.result
.data
);
509 initComponent: function() {
513 throw "no job id specified";
516 var sm
= Ext
.create('Ext.selection.TreeModel', {});
520 fields
: ['id', 'type',
524 calculate: function(data
) {
525 var txt
= 'fa x-fa-tree fa-';
526 if (data
.leaf
&& !data
.type
) {
527 return txt
+ 'hdd-o';
528 } else if (data
.type
=== 'qemu') {
529 return txt
+ 'desktop';
530 } else if (data
.type
=== 'lxc') {
533 return txt
+ 'question-circle';
541 fieldLabel
: gettext('Search'),
543 emptyText
: 'Name, VMID, Type',
546 enableKeyEvents
: true,
549 keyup: function(field
) {
550 let searchValue
= field
.getValue().toLowerCase();
551 me
.store
.clearFilter(true);
552 me
.store
.filterBy(function(record
) {
554 if (record
.data
.depth
=== 0) {
556 } else if (record
.data
.depth
=== 1) {
558 } else if (record
.data
.depth
=== 2) {
559 data
= record
.parentNode
.data
;
562 for (const property
of ['name', 'id', 'type']) {
563 if (!data
[property
]) {
566 let v
= data
[property
].toString();
567 if (v
!== undefined) {
569 if (v
.includes(searchValue
)) {
588 Ext
.define('PVE.dc.BackupInfo', {
589 extend
: 'Proxmox.panel.InputPanel',
590 alias
: 'widget.pveBackupInfo',
594 retentionType
: 'none',
597 hasRetention
: (get) => get('retentionType') !== 'none',
598 retentionKeepAll
: (get) => get('retentionType') === 'all',
606 xtype
: 'displayfield',
608 fieldLabel
: gettext('Node'),
609 renderer: function(value
) {
611 return '-- ' + gettext('All') + ' --';
618 xtype
: 'displayfield',
620 fieldLabel
: gettext('Storage'),
623 xtype
: 'displayfield',
625 fieldLabel
: gettext('Day of week'),
626 renderer
: PVE
.Utils
.render_backup_days_of_week
,
629 xtype
: 'displayfield',
631 fieldLabel
: gettext('Start Time'),
634 xtype
: 'displayfield',
636 fieldLabel
: gettext('Selection mode'),
639 xtype
: 'displayfield',
641 fieldLabel
: gettext('Pool to backup'),
646 xtype
: 'displayfield',
648 fieldLabel
: gettext('Send email to'),
651 xtype
: 'displayfield',
652 name
: 'mailnotification',
653 fieldLabel
: gettext('Email notification'),
654 renderer: function(value
) {
658 msg
= gettext('Always');
661 msg
= gettext('On failure only');
668 xtype
: 'displayfield',
670 fieldLabel
: gettext('Compression'),
673 xtype
: 'displayfield',
675 fieldLabel
: gettext('Mode'),
676 renderer: function(value
) {
680 msg
= gettext('Snapshot');
683 msg
= gettext('Suspend');
686 msg
= gettext('Stop');
693 xtype
: 'displayfield',
695 fieldLabel
: gettext('Enabled'),
696 renderer
: v
=> PVE
.Parser
.parseBoolean(v
.toString()) ? gettext('Yes') : gettext('No'),
704 text
: gettext('Retention Configuration') + ':',
706 hidden
: '{!hasRetention}',
724 xtype
: 'displayfield',
726 fieldLabel
: gettext('Keep All'),
727 renderer
: Proxmox
.Utils
.format_boolean
,
729 hidden
: '{!retentionKeepAll}',
733 ['keep-last', gettext('Keep Last')],
734 ['keep-daily', gettext('Keep Daily')],
735 ['keep-monthly', gettext('Keep Monthly')],
738 xtype
: 'displayfield',
742 hidden
: '{!hasRetention || retentionKeepAll}',
754 ['keep-hourly', gettext('Keep Hourly')],
755 ['keep-weekly', gettext('Keep Weekly')],
756 ['keep-yearly', gettext('Keep Yearly')],
759 xtype
: 'displayfield',
763 hidden
: '{!hasRetention || retentionKeepAll}',
772 setValues: function(values
) {
774 let vm
= me
.getViewModel();
776 Ext
.iterate(values
, function(fieldId
, val
) {
777 let field
= me
.query('[isFormField][name=' + fieldId
+ ']')[0];
783 if (values
['prune-backups'] || values
.maxfiles
!== undefined) {
795 if (values
['prune-backups']) {
796 keepValues
= values
['prune-backups'];
797 } else if (values
.maxfiles
> 0) {
798 keepValues
= { 'keep-last': values
.maxfiles
};
800 keepValues
= { 'keep-all': 1 };
803 vm
.set('retentionType', keepValues
['keep-all'] ? 'all' : 'other');
805 keepNames
.forEach(function(name
) {
806 let field
= me
.query('[isFormField][name=' + name
+ ']')[0];
808 field
.setValue(keepValues
[name
]);
812 vm
.set('retentionType', 'none');
815 // selection Mode depends on the presence/absence of several keys
816 let selModeField
= me
.query('[isFormField][name=selMode]')[0];
817 let selMode
= 'none';
819 selMode
= gettext('Include selected VMs');
822 selMode
= gettext('All');
824 if (values
.exclude
) {
825 selMode
= gettext('Exclude selected VMs');
828 selMode
= gettext('Pool based');
830 selModeField
.setValue(selMode
);
833 let poolField
= me
.query('[isFormField][name=pool]')[0];
834 poolField
.setVisible(0);
838 initComponent: function() {
842 throw "no data provided";
846 me
.setValues(me
.record
);
851 Ext
.define('PVE.dc.BackedGuests', {
852 extend
: 'Ext.grid.GridPanel',
853 alias
: 'widget.pveBackedGuests',
859 header
: gettext('Type'),
861 renderer
: PVE
.Utils
.render_resource_type
,
866 header
: gettext('VMID'),
872 header
: gettext('Name'),
879 initComponent: function() {
882 me
.store
.clearFilter(true);
886 stateId
: 'grid-dc-backed-guests',
889 gettext('Search') + ':', ' ',
893 emptyText
: 'Name, VMID, Type',
894 enableKeyEvents
: true,
897 keyup: function(field
) {
898 let searchValue
= field
.getValue().toLowerCase();
899 me
.store
.clearFilter(true);
900 me
.store
.filterBy(function(record
) {
901 let data
= record
.data
;
902 for (const property
in ['name', 'id', 'type']) {
903 if (data
[property
] === null) {
906 let v
= data
[property
].toString();
907 if (v
!== undefined) {
909 if (v
.includes(searchValue
)) {
929 Ext
.define('PVE.dc.BackupView', {
930 extend
: 'Ext.grid.GridPanel',
932 alias
: ['widget.pveDcBackupView'],
934 onlineHelp
: 'chapter_vzdump',
936 allText
: '-- ' + gettext('All') + ' --',
938 initComponent: function() {
941 let store
= new Ext
.data
.Store({
942 model
: 'pve-cluster-backup',
945 url
: "/api2/json/cluster/backup",
949 let not_backed_store
= new Ext
.data
.Store({
953 url
: 'api2/json/cluster/backup-info/not-backed-up',
957 let noBackupJobWarning
, noBackupJobInfoButton
;
958 let reload = function() {
960 not_backed_store
.load({
961 callback: function(records
, operation
, success
) {
962 noBackupJobWarning
.setVisible(records
.length
> 0);
963 noBackupJobInfoButton
.setVisible(records
.length
> 0);
968 let sm
= Ext
.create('Ext.selection.RowModel', {});
970 let run_editor = function() {
971 let rec
= sm
.getSelection()[0];
976 let win
= Ext
.create('PVE.dc.BackupEdit', {
979 win
.on('destroy', reload
);
983 let run_detail = function() {
984 let record
= sm
.getSelection()[0];
988 Ext
.create('Ext.window.Window', {
993 stateId
: 'backup-detail-view',
996 title
: gettext('Backup Details'),
1007 xtype
: 'pveBackupInfo',
1010 record
: record
.data
,
1013 xtype
: 'pveBackupDiskTree',
1014 title
: gettext('Included disks'),
1016 jobid
: record
.data
.id
,
1024 let run_backup_now = function(job
) {
1025 job
= Ext
.clone(job
);
1027 let jobNode
= job
.node
;
1028 // Remove properties related to scheduling
1030 delete job
.starttime
;
1034 job
.all
= job
.all
=== true ? 1 : 0;
1036 if (job
['prune-backups']) {
1037 job
['prune-backups'] = PVE
.Parser
.printPropertyString(job
['prune-backups']);
1040 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
1041 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
1044 if (jobNode
!== undefined) {
1045 if (!nodes
.includes(jobNode
)) {
1046 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
1051 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
1052 if (unkownNodes
.length
> 0) {errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));}
1054 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
1057 title
: gettext('Please wait...'),
1060 progressText
: '0/' + jobTotalCount
,
1063 let postRequest = function() {
1065 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
1067 if (jobsStarted
=== jobTotalCount
) {
1069 if (errors
.length
> 0) {
1070 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
1075 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
1076 url
: '/nodes/' + node
+ '/vzdump',
1079 failure: function(response
, opts
) {
1080 errors
.push(node
+ ': ' + response
.htmlStatus
);
1083 success
: postRequest
,
1087 let run_show_not_backed = function() {
1088 Ext
.create('Ext.window.Window', {
1094 title
: gettext('Guests without backup job'),
1105 xtype
: 'pveBackedGuests',
1108 store
: not_backed_store
,
1116 var edit_btn
= new Proxmox
.button
.Button({
1117 text
: gettext('Edit'),
1120 handler
: run_editor
,
1123 var run_btn
= new Proxmox
.button
.Button({
1124 text
: gettext('Run now'),
1127 handler: function() {
1128 var rec
= sm
.getSelection()[0];
1134 title
: gettext('Confirm'),
1135 icon
: Ext
.Msg
.QUESTION
,
1136 msg
: gettext('Start the selected backup job now?'),
1137 buttons
: Ext
.Msg
.YESNO
,
1138 callback: function(btn
) {
1139 if (btn
!== 'yes') {
1142 run_backup_now(rec
.data
);
1148 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
1150 baseurl
: '/cluster/backup',
1151 callback: function() {
1156 var detail_btn
= new Proxmox
.button
.Button({
1157 text
: gettext('Job Detail'),
1159 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
1161 handler
: run_detail
,
1164 noBackupJobWarning
= Ext
.create('Ext.toolbar.TextItem', {
1165 html
: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
1169 noBackupJobInfoButton
= new Proxmox
.button
.Button({
1170 text
: gettext('Show'),
1172 handler
: run_show_not_backed
,
1175 Proxmox
.Utils
.monStoreErrors(me
, store
);
1181 stateId
: 'grid-dc-backup',
1187 text
: gettext('Add'),
1188 handler: function() {
1189 var win
= Ext
.create('PVE.dc.BackupEdit', {});
1190 win
.on('destroy', reload
);
1202 noBackupJobInfoButton
,
1206 header
: gettext('Enabled'),
1208 dataIndex
: 'enabled',
1209 xtype
: 'checkcolumn',
1212 disabledCls
: 'x-item-enabled',
1213 stopSelection
: false,
1216 header
: gettext('Node'),
1220 renderer: function(value
) {
1228 header
: gettext('Day of week'),
1232 renderer
: PVE
.Utils
.render_backup_days_of_week
,
1235 header
: gettext('Start Time'),
1238 dataIndex
: 'starttime',
1241 header
: gettext('Storage'),
1244 dataIndex
: 'storage',
1247 header
: gettext('Selection'),
1251 renderer
: PVE
.Utils
.render_backup_selection
,
1256 itemdblclick
: run_editor
,
1263 Ext
.define('pve-cluster-backup', {
1264 extend
: 'Ext.data.Model',
1277 { name
: 'enabled', type
: 'boolean' },
1278 { name
: 'all', type
: 'boolean' },