]>
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
) {
43 let sel
= selected
.map(record
=> record
.data
.vmid
);
44 // to avoid endless recursion suspend the vmidField change
45 // event temporary as it calls us again
46 vmidField
.suspendEvent('change');
47 vmidField
.setValue(sel
);
48 vmidField
.resumeEvent('change');
53 let storagesel
= Ext
.create('PVE.form.StorageSelector', {
54 fieldLabel
: gettext('Storage'),
56 storageContent
: 'backup',
60 change: function(f
, v
) {
61 let store
= f
.getStore();
62 let rec
= store
.findRecord('storage', v
, 0, false, true, true);
63 let compressionSelector
= me
.down('pveCompressionSelector');
65 if (rec
&& rec
.data
&& rec
.data
.type
=== 'pbs') {
66 compressionSelector
.setValue('zstd');
67 compressionSelector
.setDisabled(true);
68 } else if (!compressionSelector
.getEditable()) {
69 compressionSelector
.setDisabled(false);
75 let store
= new Ext
.data
.Store({
76 model
: 'PVEResources',
83 let vmgrid
= Ext
.createWidget('grid', {
96 header
: gettext('Node'),
100 header
: gettext('Status'),
102 renderer: function(value
) {
104 return Proxmox
.Utils
.runningText
;
106 return Proxmox
.Utils
.stoppedText
;
111 header
: gettext('Name'),
116 header
: gettext('Type'),
122 let selectPoolMembers = function(poolid
) {
126 sm
.deselectAll(true);
137 let selPool
= Ext
.create('PVE.form.PoolSelector', {
138 fieldLabel
: gettext('Pool to backup'),
143 change: function(selpool
, newValue
, oldValue
) {
144 selectPoolMembers(newValue
);
149 let nodesel
= Ext
.create('PVE.form.NodeSelector', {
151 fieldLabel
: gettext('Node'),
155 emptyText
: '-- ' + gettext('All') + ' --',
157 change: function(f
, value
) {
158 storagesel
.setNodename(value
);
159 let mode
= selModeField
.getValue();
161 store
.filterBy(function(rec
) {
162 return !value
|| rec
.get('node') === value
;
164 if (mode
=== 'all') {
167 if (mode
=== 'pool') {
168 selectPoolMembers(selPool
.value
);
178 xtype
: 'pveCalendarEvent',
179 fieldLabel
: gettext('Schedule'),
190 fieldLabel
: gettext('Send email to'),
194 xtype
: 'pveEmailNotificationSelector',
195 fieldLabel
: gettext('Email'),
196 name
: 'mailnotification',
197 deleteEmpty
: !me
.isCreate
,
198 value
: me
.isCreate
? 'always' : '',
201 xtype
: 'pveCompressionSelector',
202 fieldLabel
: gettext('Compression'),
204 deleteEmpty
: !me
.isCreate
,
208 xtype
: 'pveBackupModeSelector',
209 fieldLabel
: gettext('Mode'),
214 xtype
: 'proxmoxcheckbox',
215 fieldLabel
: gettext('Enable'),
224 let ipanel
= Ext
.create('Proxmox.panel.InputPanel', {
225 onlineHelp
: 'chapter_vzdump',
230 xtype
: 'proxmoxtextfield',
232 fieldLabel
: gettext('Job Comment'),
233 deleteEmpty
: !me
.isCreate
,
236 'data-qtip': gettext('Description of the job'),
243 xtype
: 'proxmoxcheckbox',
244 fieldLabel
: gettext('Repeat missed'),
245 name
: 'repeat-missed',
248 deleteDefaultValue
: !me
.isCreate
,
251 onGetValues: function(values
) {
254 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'node' });
259 if (!values
.id
&& me
.isCreate
) {
260 values
.id
= 'backup-' + Ext
.data
.identifier
.Uuid
.Global
.generate().slice(0, 13);
263 let selMode
= values
.selMode
;
264 delete values
.selMode
;
266 if (selMode
=== 'all') {
270 } else if (selMode
=== 'exclude') {
272 values
.exclude
= values
.vmid
;
274 } else if (selMode
=== 'pool') {
278 if (selMode
!== 'pool') {
285 let update_vmid_selection = function(list
, mode
) {
286 if (mode
!== 'all' && mode
!== 'pool') {
287 sm
.deselectAll(true);
289 Ext
.Array
.each(list
.split(','), function(vmid
) {
290 var rec
= store
.findRecord('vmid', vmid
, 0, false, true, true);
292 sm
.select(rec
, true);
299 vmidField
.on('change', function(f
, value
) {
300 let mode
= selModeField
.getValue();
301 update_vmid_selection(value
, mode
);
304 selModeField
.on('change', function(f
, value
, oldValue
) {
305 if (oldValue
=== 'pool') {
306 store
.removeFilter('poolFilter');
309 if (oldValue
=== 'all') {
310 sm
.deselectAll(true);
311 vmidField
.setValue('');
314 if (value
=== 'all') {
316 vmgrid
.setDisabled(true);
318 vmgrid
.setDisabled(false);
321 if (value
=== 'pool') {
322 vmgrid
.setDisabled(true);
323 vmidField
.setValue('');
324 selPool
.setVisible(true);
325 selPool
.setDisabled(false);
326 selPool
.allowBlank
= false;
327 selectPoolMembers(selPool
.value
);
329 selPool
.setVisible(false);
330 selPool
.setDisabled(true);
331 selPool
.allowBlank
= true;
333 let list
= vmidField
.getValue();
334 update_vmid_selection(list
, value
);
337 let reload = function() {
342 callback: function() {
343 let node
= nodesel
.getValue();
345 store
.filterBy(rec
=> !node
|| node
.length
=== 0 || rec
.get('node') === node
);
346 let list
= vmidField
.getValue();
347 let mode
= selModeField
.getValue();
348 if (mode
=== 'all') {
350 } else if (mode
=== 'pool') {
351 selectPoolMembers(selPool
.value
);
353 update_vmid_selection(list
, mode
);
360 subject
: gettext("Backup Job"),
373 title
: gettext('General'),
384 xtype
: 'pveBackupJobPrunePanel',
385 title
: gettext('Retention'),
386 isCreate
: me
.isCreate
,
387 keepAllDefaultForCreate
: false,
389 fallbackHintHtml
: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
393 title
: gettext('Note Template'),
399 onGetValues: function(values
) {
400 if (values
['notes-template']) {
401 values
['notes-template'] = PVE
.Utils
.escapeNotesTemplate(
402 values
['notes-template']);
409 name
: 'notes-template',
410 fieldLabel
: gettext('Backup Notes'),
413 deleteEmpty
: !me
.isCreate
,
414 value
: me
.isCreate
? '{{guestname}}' : undefined,
420 'line-height': '1.5em',
422 html
: gettext('The notes are added to each backup created by this job.')
425 gettext('Possible template variables are: {0}'),
426 PVE
.Utils
.notesTemplateVars
.map(v
=> `<code>{{${v}}}</code>`).join(', '),
440 selModeField
.setValue('include');
443 success: function(response
, options
) {
444 let data
= response
.result
.data
;
446 data
.dow
= (data
.dow
|| '').split(',');
448 if (data
.all
|| data
.exclude
) {
450 data
.vmid
= data
.exclude
;
451 data
.selMode
= 'exclude';
454 data
.selMode
= 'all';
456 } else if (data
.pool
) {
457 data
.selMode
= 'pool';
458 data
.selPool
= data
.pool
;
460 data
.selMode
= 'include';
463 if (data
['prune-backups']) {
464 Object
.assign(data
, data
['prune-backups']);
465 delete data
['prune-backups'];
466 } else if (data
.maxfiles
!== undefined) {
467 if (data
.maxfiles
> 0) {
468 data
['keep-last'] = data
.maxfiles
;
470 data
['keep-all'] = 1;
472 delete data
.maxfiles
;
475 if (data
['notes-template']) {
476 data
['notes-template'] = PVE
.Utils
.unEscapeNotesTemplate(
477 data
['notes-template']);
489 Ext
.define('PVE.dc.BackupView', {
490 extend
: 'Ext.grid.GridPanel',
492 alias
: ['widget.pveDcBackupView'],
494 onlineHelp
: 'chapter_vzdump',
496 allText
: '-- ' + gettext('All') + ' --',
498 initComponent: function() {
501 let store
= new Ext
.data
.Store({
502 model
: 'pve-cluster-backup',
505 url
: "/api2/json/cluster/backup",
509 let not_backed_store
= new Ext
.data
.Store({
513 url
: 'api2/json/cluster/backup-info/not-backed-up',
517 let noBackupJobWarning
, noBackupJobInfoButton
;
518 let reload = function() {
520 not_backed_store
.load({
521 callback: function(records
, operation
, success
) {
522 noBackupJobWarning
.setVisible(records
.length
> 0);
523 noBackupJobInfoButton
.setVisible(records
.length
> 0);
528 let sm
= Ext
.create('Ext.selection.RowModel', {});
530 let run_editor = function() {
531 let rec
= sm
.getSelection()[0];
536 let win
= Ext
.create('PVE.dc.BackupEdit', {
539 win
.on('destroy', reload
);
543 let run_detail = function() {
544 let record
= sm
.getSelection()[0];
548 Ext
.create('Ext.window.Window', {
554 title
: gettext('Backup Details'),
565 xtype
: 'pveBackupInfo',
571 xtype
: 'pveBackupDiskTree',
572 title
: gettext('Included disks'),
574 jobid
: record
.data
.id
,
582 let run_backup_now = function(job
) {
583 job
= Ext
.clone(job
);
585 let jobNode
= job
.node
;
586 // Remove properties related to scheduling
588 delete job
.starttime
;
595 delete job
['next-run'];
596 delete job
['repeat-missed'];
597 job
.all
= job
.all
=== true ? 1 : 0;
599 ['performance', 'prune-backups'].forEach(key
=> {
601 job
[key
] = PVE
.Parser
.printPropertyString(job
[key
]);
605 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
606 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
609 if (jobNode
!== undefined) {
610 if (!nodes
.includes(jobNode
)) {
611 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
616 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
617 if (unkownNodes
.length
> 0) {errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));}
619 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
622 title
: gettext('Please wait...'),
625 progressText
: '0/' + jobTotalCount
,
628 let postRequest = function() {
630 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
632 if (jobsStarted
=== jobTotalCount
) {
634 if (errors
.length
> 0) {
635 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
640 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
641 url
: '/nodes/' + node
+ '/vzdump',
644 failure: function(response
, opts
) {
645 errors
.push(node
+ ': ' + response
.htmlStatus
);
648 success
: postRequest
,
652 let run_show_not_backed = function() {
653 Ext
.create('Ext.window.Window', {
659 title
: gettext('Guests without backup job'),
670 xtype
: 'pveBackedGuests',
673 store
: not_backed_store
,
681 var edit_btn
= new Proxmox
.button
.Button({
682 text
: gettext('Edit'),
688 var run_btn
= new Proxmox
.button
.Button({
689 text
: gettext('Run now'),
692 handler: function() {
693 var rec
= sm
.getSelection()[0];
699 title
: gettext('Confirm'),
700 icon
: Ext
.Msg
.QUESTION
,
701 msg
: gettext('Start the selected backup job now?'),
702 buttons
: Ext
.Msg
.YESNO
,
703 callback: function(btn
) {
707 run_backup_now(rec
.data
);
713 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
715 baseurl
: '/cluster/backup',
716 callback: function() {
721 var detail_btn
= new Proxmox
.button
.Button({
722 text
: gettext('Job Detail'),
724 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
729 noBackupJobWarning
= Ext
.create('Ext.toolbar.TextItem', {
730 html
: '<i class="fa fa-fw fa-exclamation-circle"></i>' + gettext('Some guests are not covered by any backup job.'),
734 noBackupJobInfoButton
= new Proxmox
.button
.Button({
735 text
: gettext('Show'),
737 handler
: run_show_not_backed
,
740 Proxmox
.Utils
.monStoreErrors(me
, store
);
746 stateId
: 'grid-dc-backup',
752 text
: gettext('Add'),
753 handler: function() {
754 var win
= Ext
.create('PVE.dc.BackupEdit', {});
755 win
.on('destroy', reload
);
767 noBackupJobInfoButton
,
770 xtype
: 'proxmoxButton',
772 text
: gettext('Schedule Simulator'),
774 let record
= sm
.getSelection()[0];
777 schedule
= record
.data
.schedule
;
779 Ext
.create('PVE.window.ScheduleSimulator', {
787 header
: gettext('Enabled'),
789 dataIndex
: 'enabled',
790 xtype
: 'checkcolumn',
793 disabledCls
: 'x-item-enabled',
794 stopSelection
: false,
797 header
: gettext('ID'),
802 header
: gettext('Node'),
806 renderer: function(value
) {
814 header
: gettext('Schedule'),
816 dataIndex
: 'schedule',
819 text
: gettext('Next Run'),
820 dataIndex
: 'next-run',
822 renderer
: PVE
.Utils
.render_next_event
,
825 header
: gettext('Storage'),
828 dataIndex
: 'storage',
831 header
: gettext('Comment'),
832 dataIndex
: 'comment',
833 renderer
: Ext
.htmlEncode
,
834 sorter
: (a
, b
) => (a
.data
.comment
|| '').localeCompare(b
.data
.comment
|| ''),
838 header
: gettext('Retention'),
839 dataIndex
: 'prune-backups',
840 renderer
: v
=> v
? PVE
.Parser
.printPropertyString(v
) : gettext('Fallback from storage config'),
844 header
: gettext('Selection'),
848 renderer
: PVE
.Utils
.render_backup_selection
,
853 itemdblclick
: run_editor
,
860 Ext
.define('pve-cluster-backup', {
861 extend
: 'Ext.data.Model',
875 { name
: 'enabled', type
: 'boolean' },
876 { name
: 'all', type
: 'boolean' },