]>
git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
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'),
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
);
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
: 'pveCalendarEvent',
183 fieldLabel
: gettext('Schedule'),
194 fieldLabel
: gettext('Send email to'),
198 xtype
: 'pveEmailNotificationSelector',
199 fieldLabel
: gettext('Email'),
200 name
: 'mailnotification',
201 deleteEmpty
: !me
.isCreate
,
202 value
: me
.isCreate
? 'always' : '',
205 xtype
: 'pveCompressionSelector',
206 fieldLabel
: gettext('Compression'),
208 deleteEmpty
: !me
.isCreate
,
212 xtype
: 'pveBackupModeSelector',
213 fieldLabel
: gettext('Mode'),
218 xtype
: 'proxmoxcheckbox',
219 fieldLabel
: gettext('Enable'),
228 let ipanel
= Ext
.create('Proxmox.panel.InputPanel', {
229 onlineHelp
: 'chapter_vzdump',
234 xtype
: 'proxmoxtextfield',
236 fieldLabel
: gettext('Comment'),
237 deleteEmpty
: !me
.isCreate
,
240 onGetValues: function(values
) {
243 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'node' });
248 if (!values
.id
&& me
.isCreate
) {
249 values
.id
= 'backup-' + Ext
.data
.identifier
.Uuid
.Global
.generate().slice(0, 13);
252 let selMode
= values
.selMode
;
253 delete values
.selMode
;
255 if (selMode
=== 'all') {
259 } else if (selMode
=== 'exclude') {
261 values
.exclude
= values
.vmid
;
263 } else if (selMode
=== 'pool') {
267 if (selMode
!== 'pool') {
274 let update_vmid_selection = function(list
, mode
) {
275 if (mode
!== 'all' && mode
!== 'pool') {
276 sm
.deselectAll(true);
278 Ext
.Array
.each(list
.split(','), function(vmid
) {
279 var rec
= store
.findRecord('vmid', vmid
, 0, false, true, true);
281 sm
.select(rec
, true);
288 vmidField
.on('change', function(f
, value
) {
289 let mode
= selModeField
.getValue();
290 update_vmid_selection(value
, mode
);
293 selModeField
.on('change', function(f
, value
, oldValue
) {
294 if (oldValue
=== 'pool') {
295 store
.removeFilter('poolFilter');
298 if (oldValue
=== 'all') {
299 sm
.deselectAll(true);
300 vmidField
.setValue('');
303 if (value
=== 'all') {
305 vmgrid
.setDisabled(true);
307 vmgrid
.setDisabled(false);
310 if (value
=== 'pool') {
311 vmgrid
.setDisabled(true);
312 vmidField
.setValue('');
313 selPool
.setVisible(true);
314 selPool
.allowBlank
= false;
315 selectPoolMembers(selPool
.value
);
317 selPool
.setVisible(false);
318 selPool
.allowBlank
= true;
320 let list
= vmidField
.getValue();
321 update_vmid_selection(list
, value
);
324 let reload = function() {
329 callback: function() {
330 let node
= nodesel
.getValue();
332 store
.filterBy(rec
=> !node
|| node
.length
=== 0 || rec
.get('node') === node
);
333 let list
= vmidField
.getValue();
334 let mode
= selModeField
.getValue();
335 if (mode
=== 'all') {
337 } else if (mode
=== 'pool') {
338 selectPoolMembers(selPool
.value
);
340 update_vmid_selection(list
, mode
);
347 subject
: gettext("Backup Job"),
360 title
: gettext('General'),
372 xtype
: 'pveBackupJobPrunePanel',
373 title
: gettext('Retention'),
374 isCreate
: me
.isCreate
,
375 keepAllDefaultForCreate
: false,
377 fallbackHintHtml
: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
388 selModeField
.setValue('include');
391 success: function(response
, options
) {
392 let data
= response
.result
.data
;
394 data
.dow
= (data
.dow
|| '').split(',');
396 if (data
.all
|| data
.exclude
) {
398 data
.vmid
= data
.exclude
;
399 data
.selMode
= 'exclude';
402 data
.selMode
= 'all';
404 } else if (data
.pool
) {
405 data
.selMode
= 'pool';
406 data
.selPool
= data
.pool
;
408 data
.selMode
= 'include';
411 if (data
['prune-backups']) {
412 Object
.assign(data
, data
['prune-backups']);
413 delete data
['prune-backups'];
414 } else if (data
.maxfiles
!== undefined) {
415 if (data
.maxfiles
> 0) {
416 data
['keep-last'] = data
.maxfiles
;
418 data
['keep-all'] = 1;
420 delete data
.maxfiles
;
432 Ext
.define('PVE.dc.BackupView', {
433 extend
: 'Ext.grid.GridPanel',
435 alias
: ['widget.pveDcBackupView'],
437 onlineHelp
: 'chapter_vzdump',
439 allText
: '-- ' + gettext('All') + ' --',
441 initComponent: function() {
444 let store
= new Ext
.data
.Store({
445 model
: 'pve-cluster-backup',
448 url
: "/api2/json/cluster/backup",
452 let not_backed_store
= new Ext
.data
.Store({
456 url
: 'api2/json/cluster/backup-info/not-backed-up',
460 let noBackupJobWarning
, noBackupJobInfoButton
;
461 let reload = function() {
463 not_backed_store
.load({
464 callback: function(records
, operation
, success
) {
465 noBackupJobWarning
.setVisible(records
.length
> 0);
466 noBackupJobInfoButton
.setVisible(records
.length
> 0);
471 let sm
= Ext
.create('Ext.selection.RowModel', {});
473 let run_editor = function() {
474 let rec
= sm
.getSelection()[0];
479 let win
= Ext
.create('PVE.dc.BackupEdit', {
482 win
.on('destroy', reload
);
486 let run_detail = function() {
487 let record
= sm
.getSelection()[0];
491 Ext
.create('Ext.window.Window', {
496 stateId
: 'backup-detail-view',
499 title
: gettext('Backup Details'),
510 xtype
: 'pveBackupInfo',
516 xtype
: 'pveBackupDiskTree',
517 title
: gettext('Included disks'),
519 jobid
: record
.data
.id
,
527 let run_backup_now = function(job
) {
528 job
= Ext
.clone(job
);
530 let jobNode
= job
.node
;
531 // Remove properties related to scheduling
533 delete job
.starttime
;
539 job
.all
= job
.all
=== true ? 1 : 0;
541 if (job
['prune-backups']) {
542 job
['prune-backups'] = PVE
.Parser
.printPropertyString(job
['prune-backups']);
545 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
546 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
549 if (jobNode
!== undefined) {
550 if (!nodes
.includes(jobNode
)) {
551 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
556 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
557 if (unkownNodes
.length
> 0) {errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));}
559 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
562 title
: gettext('Please wait...'),
565 progressText
: '0/' + jobTotalCount
,
568 let postRequest = function() {
570 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
572 if (jobsStarted
=== jobTotalCount
) {
574 if (errors
.length
> 0) {
575 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
580 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
581 url
: '/nodes/' + node
+ '/vzdump',
584 failure: function(response
, opts
) {
585 errors
.push(node
+ ': ' + response
.htmlStatus
);
588 success
: postRequest
,
592 let run_show_not_backed = function() {
593 Ext
.create('Ext.window.Window', {
599 title
: gettext('Guests without backup job'),
610 xtype
: 'pveBackedGuests',
613 store
: not_backed_store
,
621 var edit_btn
= new Proxmox
.button
.Button({
622 text
: gettext('Edit'),
628 var run_btn
= new Proxmox
.button
.Button({
629 text
: gettext('Run now'),
632 handler: function() {
633 var rec
= sm
.getSelection()[0];
639 title
: gettext('Confirm'),
640 icon
: Ext
.Msg
.QUESTION
,
641 msg
: gettext('Start the selected backup job now?'),
642 buttons
: Ext
.Msg
.YESNO
,
643 callback: function(btn
) {
647 run_backup_now(rec
.data
);
653 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
655 baseurl
: '/cluster/backup',
656 callback: function() {
661 var detail_btn
= new Proxmox
.button
.Button({
662 text
: gettext('Job Detail'),
664 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
669 noBackupJobWarning
= Ext
.create('Ext.toolbar.TextItem', {
670 html
: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
674 noBackupJobInfoButton
= new Proxmox
.button
.Button({
675 text
: gettext('Show'),
677 handler
: run_show_not_backed
,
680 Proxmox
.Utils
.monStoreErrors(me
, store
);
686 stateId
: 'grid-dc-backup',
692 text
: gettext('Add'),
693 handler: function() {
694 var win
= Ext
.create('PVE.dc.BackupEdit', {});
695 win
.on('destroy', reload
);
705 xtype
: 'proxmoxButton',
707 text
: gettext('Schedule Simulator'),
709 let record
= sm
.getSelection()[0];
712 schedule
= record
.data
.schedule
;
714 Ext
.create('PVE.window.ScheduleSimulator', {
722 noBackupJobInfoButton
,
726 header
: gettext('Enabled'),
728 dataIndex
: 'enabled',
729 xtype
: 'checkcolumn',
732 disabledCls
: 'x-item-enabled',
733 stopSelection
: false,
736 header
: gettext('ID'),
741 header
: gettext('Node'),
745 renderer: function(value
) {
753 header
: gettext('Schedule'),
755 dataIndex
: 'schedule',
758 header
: gettext('Storage'),
761 dataIndex
: 'storage',
764 header
: gettext('Comment'),
765 dataIndex
: 'comment',
766 renderer
: Ext
.htmlEncode
,
767 sorter
: (a
, b
) => (a
.data
.comment
|| '').localeCompare(b
.data
.comment
|| ''),
771 header
: gettext('Selection'),
775 renderer
: PVE
.Utils
.render_backup_selection
,
780 itemdblclick
: run_editor
,
787 Ext
.define('pve-cluster-backup', {
788 extend
: 'Ext.data.Model',
801 { name
: 'enabled', type
: 'boolean' },
802 { name
: 'all', type
: 'boolean' },