]>
git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
9907695738f92baa7fe14222496daecf4092729a
1 Ext
.define('PVE.dc.BackupEdit', {
2 extend
: 'Proxmox.window.Edit',
3 alias
: ['widget.pveDcBackupEdit'],
5 mixins
: ['Proxmox.Mixin.CBind'],
7 defaultFocus
: undefined,
9 subject
: gettext("Backup Job"),
12 url
: '/api2/extjs/cluster/backup',
16 cbindData: function() {
21 me
.url
+= `/${me.jobid}`;
27 xclass
: 'Ext.app.ViewController',
29 onGetValues: function(values
) {
31 let isCreate
= me
.getView().isCreate
;
34 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'node' });
40 // 'mailnotification' is deprecated in favor of 'notification-policy'
41 // -> Migration to the new parameter happens in init, so we are
42 // safe to remove the old parameter here.
43 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'mailnotification' });
45 // If sending notifications via mail, remove the current value of
46 // 'notification-target'
47 if (values
['notification-mode'] === "mailto") {
48 Proxmox
.Utils
.assemble_field_data(
50 { 'delete': 'notification-target' },
54 Proxmox
.Utils
.assemble_field_data(
56 { 'delete': 'mailto' },
61 delete values
['notification-mode'];
63 if (!values
.id
&& isCreate
) {
64 values
.id
= 'backup-' + Ext
.data
.identifier
.Uuid
.Global
.generate().slice(0, 13);
67 let selMode
= values
.selMode
;
68 delete values
.selMode
;
70 if (selMode
=== 'all') {
74 } else if (selMode
=== 'exclude') {
76 values
.exclude
= values
.vmid
;
78 } else if (selMode
=== 'pool') {
82 if (selMode
!== 'pool') {
88 nodeChange: function(f
, value
) {
90 me
.lookup('storageSelector').setNodename(value
);
91 let vmgrid
= me
.lookup('vmgrid');
92 let store
= vmgrid
.getStore();
95 store
.filterBy(function(rec
) {
96 return !value
|| rec
.get('node') === value
;
99 let mode
= me
.lookup('modeSelector').getValue();
100 if (mode
=== 'all') {
101 vmgrid
.selModel
.selectAll(true);
103 if (mode
=== 'pool') {
104 me
.selectPoolMembers();
108 storageChange: function(f
, v
) {
110 let rec
= f
.getStore().findRecord('storage', v
, 0, false, true, true);
111 let compressionSelector
= me
.lookup('compressionSelector');
113 if (rec
?.data
?.type
=== 'pbs') {
114 compressionSelector
.setValue('zstd');
115 compressionSelector
.setDisabled(true);
116 } else if (!compressionSelector
.getEditable()) {
117 compressionSelector
.setDisabled(false);
121 selectPoolMembers: function() {
123 let mode
= me
.lookup('modeSelector').getValue();
125 if (mode
!== 'pool') {
129 let vmgrid
= me
.lookup('vmgrid');
130 let poolid
= me
.lookup('poolSelector').getValue();
132 vmgrid
.getSelectionModel().deselectAll(true);
136 vmgrid
.getStore().filter([
143 vmgrid
.selModel
.selectAll(true);
146 modeChange: function(f
, value
, oldValue
) {
148 let vmgrid
= me
.lookup('vmgrid');
149 vmgrid
.getStore().removeFilter('poolFilter');
151 if (oldValue
=== 'all' && value
!== 'all') {
152 vmgrid
.getSelectionModel().deselectAll(true);
155 if (value
=== 'all') {
156 vmgrid
.getSelectionModel().selectAll(true);
159 if (value
=== 'pool') {
160 me
.selectPoolMembers();
164 init: function(view
) {
167 me
.lookup('modeSelector').setValue('include');
170 success: function(response
, _options
) {
171 let data
= response
.result
.data
;
173 // 'mailnotification' is deprecated. Let's automatically
174 // migrate to the compatible 'notification-policy' parameter
175 if (data
.mailnotification
) {
176 if (!data
["notification-policy"]) {
177 data
["notification-policy"] = data
.mailnotification
;
180 delete data
.mailnotification
;
183 if (data
['notification-target']) {
184 data
['notification-mode'] = 'notification-target';
185 } else if (data
.mailto
) {
186 data
['notification-mode'] = 'mailto';
190 data
.vmid
= data
.exclude
;
191 data
.selMode
= 'exclude';
192 } else if (data
.all
) {
194 data
.selMode
= 'all';
195 } else if (data
.pool
) {
196 data
.selMode
= 'pool';
197 data
.selPool
= data
.pool
;
199 data
.selMode
= 'include';
202 me
.getViewModel().set('selMode', data
.selMode
);
204 if (data
['prune-backups']) {
205 Object
.assign(data
, data
['prune-backups']);
206 delete data
['prune-backups'];
207 } else if (data
.maxfiles
!== undefined) {
208 if (data
.maxfiles
> 0) {
209 data
['keep-last'] = data
.maxfiles
;
211 data
['keep-all'] = 1;
213 delete data
.maxfiles
;
216 if (data
['notes-template']) {
217 data
['notes-template'] =
218 PVE
.Utils
.unEscapeNotesTemplate(data
['notes-template']);
221 view
.setValues(data
);
231 notificationMode
: 'notification-target',
235 poolMode
: (get) => get('selMode') === 'pool',
236 disableVMSelection
: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude',
237 mailNotificationSelected
: (get) => get('notificationMode') === 'mailto',
250 title
: gettext('General'),
259 onlineHelp
: 'chapter_vzdump',
262 xtype
: 'pveNodeSelector',
264 fieldLabel
: gettext('Node'),
268 emptyText
: '-- ' + gettext('All') + ' --',
270 change
: 'nodeChange',
274 xtype
: 'pveStorageSelector',
275 reference
: 'storageSelector',
276 fieldLabel
: gettext('Storage'),
278 storageContent
: 'backup',
282 change
: 'storageChange',
286 xtype
: 'pveCalendarEvent',
287 fieldLabel
: gettext('Schedule'),
292 xtype
: 'proxmoxKVComboBox',
293 reference
: 'modeSelector',
295 ['include', gettext('Include selected VMs')],
296 ['all', gettext('All')],
297 ['exclude', gettext('Exclude selected VMs')],
298 ['pool', gettext('Pool based')],
300 fieldLabel
: gettext('Selection mode'),
307 change
: 'modeChange',
311 xtype
: 'pvePoolSelector',
312 reference
: 'poolSelector',
313 fieldLabel
: gettext('Pool to backup'),
318 change
: 'selectPoolMembers',
321 hidden
: '{!poolMode}',
322 disabled
: '{!poolMode}',
328 xtype
: 'pveEmailNotificationSelector',
329 fieldLabel
: gettext('Notify'),
330 name
: 'notification-policy',
332 value
: (get) => get('isCreate') ? 'always' : '',
333 deleteEmpty
: '{!isCreate}',
337 xtype
: 'pveNotificationModeSelector',
338 fieldLabel
: gettext('Notify via'),
339 name
: 'notification-mode',
341 value
: '{notificationMode}',
345 xtype
: 'pveNotificationTargetSelector',
346 fieldLabel
: gettext('Notification Target'),
347 name
: 'notification-target',
352 hidden
: '{mailNotificationSelected}',
353 disabled
: '{mailNotificationSelected}',
356 deleteEmpty
: '{!isCreate}',
361 fieldLabel
: gettext('Send email to'),
365 hidden
: '{!mailNotificationSelected}',
366 disabled
: '{!mailNotificationSelected}',
370 xtype
: 'pveCompressionSelector',
371 reference
: 'compressionSelector',
372 fieldLabel
: gettext('Compression'),
375 deleteEmpty
: '{!isCreate}',
380 xtype
: 'pveBackupModeSelector',
381 fieldLabel
: gettext('Mode'),
386 xtype
: 'proxmoxcheckbox',
387 fieldLabel
: gettext('Enable'),
396 xtype
: 'proxmoxtextfield',
398 fieldLabel
: gettext('Job Comment'),
400 deleteEmpty
: '{!isCreate}',
404 'data-qtip': gettext('Description of the job'),
414 columnSelection
: ['vmid', 'node', 'status', 'name', 'type'],
416 disabled
: '{disableVMSelection}',
422 xtype
: 'proxmoxcheckbox',
423 fieldLabel
: gettext('Repeat missed'),
424 name
: 'repeat-missed',
428 deleteDefaultValue
: '{!isCreate}',
432 onGetValues: function(values
) {
433 return this.up('window').getController().onGetValues(values
);
439 xtype
: 'pveBackupJobPrunePanel',
440 title
: gettext('Retention'),
442 isCreate
: '{isCreate}',
444 keepAllDefaultForCreate
: false,
446 fallbackHintHtml
: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
450 title
: gettext('Note Template'),
456 onGetValues: function(values
) {
457 if (values
['notes-template']) {
458 values
['notes-template'] =
459 PVE
.Utils
.escapeNotesTemplate(values
['notes-template']);
466 name
: 'notes-template',
467 fieldLabel
: gettext('Backup Notes'),
471 deleteEmpty
: '{!isCreate}',
472 value
: (get) => get('isCreate') ? '{{guestname}}' : undefined,
479 'line-height': '1.5em',
481 html
: gettext('The notes are added to each backup created by this job.')
484 gettext('Possible template variables are: {0}'),
485 PVE
.Utils
.notesTemplateVars
.map(v
=> `<code>{{${v}}}</code>`).join(', '),
495 Ext
.define('PVE.dc.BackupView', {
496 extend
: 'Ext.grid.GridPanel',
498 alias
: ['widget.pveDcBackupView'],
500 onlineHelp
: 'chapter_vzdump',
502 allText
: '-- ' + gettext('All') + ' --',
504 initComponent: function() {
507 let store
= new Ext
.data
.Store({
508 model
: 'pve-cluster-backup',
511 url
: "/api2/json/cluster/backup",
515 let not_backed_store
= new Ext
.data
.Store({
519 url
: 'api2/json/cluster/backup-info/not-backed-up',
523 let noBackupJobInfoButton
;
524 let reload = function() {
526 not_backed_store
.load({
527 callback
: records
=> noBackupJobInfoButton
.setVisible(records
.length
> 0),
531 let sm
= Ext
.create('Ext.selection.RowModel', {});
533 let run_editor = function() {
534 let rec
= sm
.getSelection()[0];
539 let win
= Ext
.create('PVE.dc.BackupEdit', {
542 win
.on('destroy', reload
);
546 let run_detail = function() {
547 let record
= sm
.getSelection()[0];
551 Ext
.create('Ext.window.Window', {
554 height
: Ext
.getBody().getViewSize().height
> 1000 ? 800 : 600, // factor out as common infra?
557 title
: gettext('Backup Details'),
568 xtype
: 'pveBackupInfo',
574 xtype
: 'pveBackupDiskTree',
575 title
: gettext('Included disks'),
577 jobid
: record
.data
.id
,
585 let run_backup_now = function(job
) {
586 job
= Ext
.clone(job
);
588 let jobNode
= job
.node
;
589 // Remove properties related to scheduling
591 delete job
.starttime
;
598 delete job
['next-run'];
599 delete job
['repeat-missed'];
600 job
.all
= job
.all
=== true ? 1 : 0;
602 ['performance', 'prune-backups'].forEach(key
=> {
604 job
[key
] = PVE
.Parser
.printPropertyString(job
[key
]);
608 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
609 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
612 if (jobNode
!== undefined) {
613 if (!nodes
.includes(jobNode
)) {
614 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
619 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
620 if (unkownNodes
.length
> 0) {errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));}
622 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
625 title
: gettext('Please wait...'),
628 progressText
: '0/' + jobTotalCount
,
631 let postRequest = function() {
633 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
635 if (jobsStarted
=== jobTotalCount
) {
637 if (errors
.length
> 0) {
638 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
643 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
644 url
: '/nodes/' + node
+ '/vzdump',
647 failure: function(response
, opts
) {
648 errors
.push(node
+ ': ' + response
.htmlStatus
);
651 success
: postRequest
,
655 var edit_btn
= new Proxmox
.button
.Button({
656 text
: gettext('Edit'),
662 var run_btn
= new Proxmox
.button
.Button({
663 text
: gettext('Run now'),
666 handler: function() {
667 var rec
= sm
.getSelection()[0];
673 title
: gettext('Confirm'),
674 icon
: Ext
.Msg
.QUESTION
,
675 msg
: gettext('Start the selected backup job now?'),
676 buttons
: Ext
.Msg
.YESNO
,
677 callback: function(btn
) {
681 run_backup_now(rec
.data
);
687 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
689 baseurl
: '/cluster/backup',
690 callback: function() {
695 var detail_btn
= new Proxmox
.button
.Button({
696 text
: gettext('Job Detail'),
698 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
703 noBackupJobInfoButton
= new Proxmox
.button
.Button({
704 text
: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
705 tooltip
: gettext('Some guests are not covered by any backup job.'),
706 iconCls
: 'fa fa-fw fa-exclamation-circle',
709 Ext
.create('Ext.window.Window', {
716 title
: gettext('Guests Without Backup Job'),
727 xtype
: 'pveBackedGuests',
730 store
: not_backed_store
,
739 Proxmox
.Utils
.monStoreErrors(me
, store
);
745 stateId
: 'grid-dc-backup',
751 overflowHandler
: 'scroller',
755 text
: gettext('Add'),
756 handler: function() {
757 var win
= Ext
.create('PVE.dc.BackupEdit', {});
758 win
.on('destroy', reload
);
769 noBackupJobInfoButton
,
772 xtype
: 'proxmoxButton',
774 text
: gettext('Schedule Simulator'),
776 let record
= sm
.getSelection()[0];
779 schedule
= record
.data
.schedule
;
781 Ext
.create('PVE.window.ScheduleSimulator', {
791 header
: gettext('Enabled'),
793 dataIndex
: 'enabled',
795 renderer
: Proxmox
.Utils
.renderEnabledIcon
,
799 header
: gettext('ID'),
804 header
: gettext('Node'),
808 renderer: function(value
) {
816 header
: gettext('Schedule'),
818 dataIndex
: 'schedule',
821 text
: gettext('Next Run'),
822 dataIndex
: 'next-run',
824 renderer
: PVE
.Utils
.render_next_event
,
827 header
: gettext('Storage'),
830 dataIndex
: 'storage',
833 header
: gettext('Comment'),
834 dataIndex
: 'comment',
835 renderer
: Ext
.htmlEncode
,
836 sorter
: (a
, b
) => (a
.data
.comment
|| '').localeCompare(b
.data
.comment
|| ''),
840 header
: gettext('Retention'),
841 dataIndex
: 'prune-backups',
842 renderer
: v
=> v
? PVE
.Parser
.printPropertyString(v
) : gettext('Fallback from storage config'),
846 header
: gettext('Selection'),
850 renderer
: PVE
.Utils
.render_backup_selection
,
855 itemdblclick
: run_editor
,
862 Ext
.define('pve-cluster-backup', {
863 extend
: 'Ext.data.Model',
877 { name
: 'enabled', type
: 'boolean' },
878 { name
: 'all', type
: 'boolean' },