]>
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 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' });
39 // Get rid of new-old parameters for notification settings.
40 // These should only be set for those selected few who ran
41 // pve-manager from pvetest.
43 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'notification-policy' });
44 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'notification-target' });
47 if (!values
.id
&& isCreate
) {
48 values
.id
= 'backup-' + Ext
.data
.identifier
.Uuid
.Global
.generate().slice(0, 13);
51 let selMode
= values
.selMode
;
52 delete values
.selMode
;
54 if (selMode
=== 'all') {
58 } else if (selMode
=== 'exclude') {
60 values
.exclude
= values
.vmid
;
62 } else if (selMode
=== 'pool') {
66 if (selMode
!== 'pool') {
72 nodeChange: function(f
, value
) {
74 me
.lookup('storageSelector').setNodename(value
);
75 let vmgrid
= me
.lookup('vmgrid');
76 let store
= vmgrid
.getStore();
79 store
.filterBy(function(rec
) {
80 return !value
|| rec
.get('node') === value
;
83 let mode
= me
.lookup('modeSelector').getValue();
85 vmgrid
.selModel
.selectAll(true);
87 if (mode
=== 'pool') {
88 me
.selectPoolMembers();
92 storageChange: function(f
, v
) {
94 let rec
= f
.getStore().findRecord('storage', v
, 0, false, true, true);
95 let compressionSelector
= me
.lookup('compressionSelector');
97 if (rec
?.data
?.type
=== 'pbs') {
98 compressionSelector
.setValue('zstd');
99 compressionSelector
.setDisabled(true);
100 } else if (!compressionSelector
.getEditable()) {
101 compressionSelector
.setDisabled(false);
105 selectPoolMembers: function() {
107 let mode
= me
.lookup('modeSelector').getValue();
109 if (mode
!== 'pool') {
113 let vmgrid
= me
.lookup('vmgrid');
114 let poolid
= me
.lookup('poolSelector').getValue();
116 vmgrid
.getSelectionModel().deselectAll(true);
120 vmgrid
.getStore().filter([
127 vmgrid
.selModel
.selectAll(true);
130 modeChange: function(f
, value
, oldValue
) {
132 let vmgrid
= me
.lookup('vmgrid');
133 vmgrid
.getStore().removeFilter('poolFilter');
135 if (oldValue
=== 'all' && value
!== 'all') {
136 vmgrid
.getSelectionModel().deselectAll(true);
139 if (value
=== 'all') {
140 vmgrid
.getSelectionModel().selectAll(true);
143 if (value
=== 'pool') {
144 me
.selectPoolMembers();
148 compressionChange: function(f
, value
, oldValue
) {
149 this.getView().lookup('backupAdvanced').updateCompression(value
, f
.isDisabled());
152 compressionDisable: function(f
) {
153 this.getView().lookup('backupAdvanced').updateCompression(f
.getValue(), true);
156 compressionEnable: function(f
) {
157 this.getView().lookup('backupAdvanced').updateCompression(f
.getValue(), false);
160 init: function(view
) {
163 me
.lookup('modeSelector').setValue('include');
166 success: function(response
, _options
) {
167 let data
= response
.result
.data
;
169 // Migrate 'new'-old notification-policy back to
170 // old-old mailnotification. Only should affect
171 // users who used pve-manager from pvetest.
172 // This was a remnant of notifications before the
174 let policy
= data
['notification-policy'];
175 if (policy
=== 'always' || policy
=== 'failure') {
176 data
.mailnotification
= policy
;
180 data
.vmid
= data
.exclude
;
181 data
.selMode
= 'exclude';
182 } else if (data
.all
) {
184 data
.selMode
= 'all';
185 } else if (data
.pool
) {
186 data
.selMode
= 'pool';
187 data
.selPool
= data
.pool
;
189 data
.selMode
= 'include';
192 me
.getViewModel().set('selMode', data
.selMode
);
194 if (data
['prune-backups']) {
195 Object
.assign(data
, data
['prune-backups']);
196 delete data
['prune-backups'];
197 } else if (data
.maxfiles
!== undefined) {
198 if (data
.maxfiles
> 0) {
199 data
['keep-last'] = data
.maxfiles
;
201 data
['keep-all'] = 1;
203 delete data
.maxfiles
;
206 if (data
['notes-template']) {
207 data
['notes-template'] =
208 PVE
.Utils
.unEscapeNotesTemplate(data
['notes-template']);
211 if (data
.performance
) {
212 Object
.assign(data
, data
.performance
);
213 delete data
.performance
;
216 view
.setValues(data
);
226 notificationMode
: '__default__',
230 poolMode
: (get) => get('selMode') === 'pool',
231 disableVMSelection
: (get) => get('selMode') !== 'include' && get('selMode') !== 'exclude',
232 showMailtoFields
: (get) =>
233 ['auto', 'legacy-sendmail', '__default__'].includes(get('notificationMode')),
246 title
: gettext('General'),
255 onlineHelp
: 'chapter_vzdump',
258 xtype
: 'pveNodeSelector',
260 fieldLabel
: gettext('Node'),
264 emptyText
: '-- ' + gettext('All') + ' --',
266 change
: 'nodeChange',
270 xtype
: 'pveStorageSelector',
271 reference
: 'storageSelector',
272 fieldLabel
: gettext('Storage'),
274 storageContent
: 'backup',
278 change
: 'storageChange',
282 xtype
: 'pveCalendarEvent',
283 fieldLabel
: gettext('Schedule'),
288 xtype
: 'proxmoxKVComboBox',
289 reference
: 'modeSelector',
291 ['include', gettext('Include selected VMs')],
292 ['all', gettext('All')],
293 ['exclude', gettext('Exclude selected VMs')],
294 ['pool', gettext('Pool based')],
296 fieldLabel
: gettext('Selection mode'),
303 change
: 'modeChange',
307 xtype
: 'pvePoolSelector',
308 reference
: 'poolSelector',
309 fieldLabel
: gettext('Pool to backup'),
314 change
: 'selectPoolMembers',
317 hidden
: '{!poolMode}',
318 disabled
: '{!poolMode}',
324 xtype
: 'proxmoxKVComboBox',
329 gettext('{0} (Auto)'), Proxmox
.Utils
.defaultText
,
332 ['auto', gettext('Auto')],
333 ['legacy-sendmail', gettext('Email (legacy)')],
334 ['notification-system', gettext('Notification system')],
336 fieldLabel
: gettext('Notification mode'),
337 name
: 'notification-mode',
339 deleteEmpty
: '{!isCreate}',
342 value
: '{notificationMode}',
346 xtype
: 'pveEmailNotificationSelector',
347 fieldLabel
: gettext('Send email'),
348 name
: 'mailnotification',
350 value
: (get) => get('isCreate') ? 'always' : '',
351 deleteEmpty
: '{!isCreate}',
354 disabled
: '{!showMailtoFields}',
359 fieldLabel
: gettext('Send email to'),
362 disabled
: '{!showMailtoFields}',
366 xtype
: 'pveBackupCompressionSelector',
367 reference
: 'compressionSelector',
368 fieldLabel
: gettext('Compression'),
371 deleteEmpty
: '{!isCreate}',
375 change
: 'compressionChange',
376 disable
: 'compressionDisable',
377 enable
: 'compressionEnable',
381 xtype
: 'pveBackupModeSelector',
382 fieldLabel
: gettext('Mode'),
387 xtype
: 'proxmoxcheckbox',
388 fieldLabel
: gettext('Enable'),
397 xtype
: 'proxmoxtextfield',
399 fieldLabel
: gettext('Job Comment'),
401 deleteEmpty
: '{!isCreate}',
405 'data-qtip': gettext('Description of the job'),
415 columnSelection
: ['vmid', 'node', 'status', 'name', 'type'],
417 disabled
: '{disableVMSelection}',
423 xtype
: 'proxmoxcheckbox',
424 fieldLabel
: gettext('Repeat missed'),
425 name
: 'repeat-missed',
429 deleteDefaultValue
: '{!isCreate}',
433 onGetValues: function(values
) {
434 return this.up('window').getController().onGetValues(values
);
440 xtype
: 'pveBackupJobPrunePanel',
441 title
: gettext('Retention'),
443 isCreate
: '{isCreate}',
445 keepAllDefaultForCreate
: false,
447 fallbackHintHtml
: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
451 title
: gettext('Note Template'),
457 onGetValues: function(values
) {
458 if (values
['notes-template']) {
459 values
['notes-template'] =
460 PVE
.Utils
.escapeNotesTemplate(values
['notes-template']);
467 name
: 'notes-template',
468 fieldLabel
: gettext('Backup Notes'),
472 deleteEmpty
: '{!isCreate}',
473 value
: (get) => get('isCreate') ? '{{guestname}}' : undefined,
480 'line-height': '1.5em',
482 html
: gettext('The notes are added to each backup created by this job.')
485 gettext('Possible template variables are: {0}'),
486 PVE
.Utils
.notesTemplateVars
.map(v
=> `<code>{{${v}}}</code>`).join(', '),
492 xtype
: 'pveBackupAdvancedOptionsPanel',
493 reference
: 'backupAdvanced',
494 title
: gettext('Advanced'),
496 isCreate
: '{isCreate}',
504 Ext
.define('PVE.dc.BackupView', {
505 extend
: 'Ext.grid.GridPanel',
507 alias
: ['widget.pveDcBackupView'],
509 onlineHelp
: 'chapter_vzdump',
511 allText
: '-- ' + gettext('All') + ' --',
513 initComponent: function() {
516 let store
= new Ext
.data
.Store({
517 model
: 'pve-cluster-backup',
520 url
: "/api2/json/cluster/backup",
524 let not_backed_store
= new Ext
.data
.Store({
528 url
: 'api2/json/cluster/backup-info/not-backed-up',
532 let noBackupJobInfoButton
;
533 let reload = function() {
535 not_backed_store
.load({
536 callback
: records
=> noBackupJobInfoButton
.setVisible(records
.length
> 0),
540 let sm
= Ext
.create('Ext.selection.RowModel', {});
542 let run_editor = function() {
543 let rec
= sm
.getSelection()[0];
548 Ext
.create('PVE.dc.BackupEdit', {
552 destroy
: () => reload(),
557 let run_detail = function() {
558 let record
= sm
.getSelection()[0];
562 Ext
.create('Ext.window.Window', {
565 height
: Ext
.getBody().getViewSize().height
> 1000 ? 800 : 600, // factor out as common infra?
568 title
: gettext('Backup Details'),
579 xtype
: 'pveBackupInfo',
585 xtype
: 'pveBackupDiskTree',
586 title
: gettext('Included disks'),
588 jobid
: record
.data
.id
,
596 let run_backup_now = function(job
) {
597 job
= Ext
.clone(job
);
599 let jobNode
= job
.node
;
600 // Remove properties related to scheduling
602 delete job
.starttime
;
609 delete job
['next-run'];
610 delete job
['repeat-missed'];
611 job
.all
= job
.all
=== true ? 1 : 0;
613 ['performance', 'prune-backups'].forEach(key
=> {
615 job
[key
] = PVE
.Parser
.printPropertyString(job
[key
]);
619 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
620 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
623 if (jobNode
!== undefined) {
624 if (!nodes
.includes(jobNode
)) {
625 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
630 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
631 if (unkownNodes
.length
> 0) {errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));}
633 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
636 title
: gettext('Please wait...'),
639 progressText
: '0/' + jobTotalCount
,
642 let postRequest = function() {
644 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
646 if (jobsStarted
=== jobTotalCount
) {
648 if (errors
.length
> 0) {
649 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
654 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
655 url
: '/nodes/' + node
+ '/vzdump',
658 failure: function(response
, opts
) {
659 errors
.push(node
+ ': ' + response
.htmlStatus
);
662 success
: postRequest
,
666 var edit_btn
= new Proxmox
.button
.Button({
667 text
: gettext('Edit'),
673 var run_btn
= new Proxmox
.button
.Button({
674 text
: gettext('Run now'),
677 handler: function() {
678 var rec
= sm
.getSelection()[0];
684 title
: gettext('Confirm'),
685 icon
: Ext
.Msg
.QUESTION
,
686 msg
: gettext('Start the selected backup job now?'),
687 buttons
: Ext
.Msg
.YESNO
,
688 callback: function(btn
) {
692 run_backup_now(rec
.data
);
698 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
700 baseurl
: '/cluster/backup',
701 callback: function() {
706 var detail_btn
= new Proxmox
.button
.Button({
707 text
: gettext('Job Detail'),
709 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
714 noBackupJobInfoButton
= new Proxmox
.button
.Button({
715 text
: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
716 tooltip
: gettext('Some guests are not covered by any backup job.'),
717 iconCls
: 'fa fa-fw fa-exclamation-circle',
720 Ext
.create('Ext.window.Window', {
727 title
: gettext('Guests Without Backup Job'),
738 xtype
: 'pveBackedGuests',
741 store
: not_backed_store
,
750 Proxmox
.Utils
.monStoreErrors(me
, store
);
756 stateId
: 'grid-dc-backup',
762 overflowHandler
: 'scroller',
766 text
: gettext('Add'),
767 handler: function() {
768 var win
= Ext
.create('PVE.dc.BackupEdit', {});
769 win
.on('destroy', reload
);
780 noBackupJobInfoButton
,
783 xtype
: 'proxmoxButton',
785 text
: gettext('Schedule Simulator'),
787 let record
= sm
.getSelection()[0];
790 schedule
= record
.data
.schedule
;
792 Ext
.create('PVE.window.ScheduleSimulator', {
802 header
: gettext('Enabled'),
804 dataIndex
: 'enabled',
806 renderer
: Proxmox
.Utils
.renderEnabledIcon
,
810 header
: gettext('ID'),
815 header
: gettext('Node'),
819 renderer: function(value
) {
827 header
: gettext('Schedule'),
829 dataIndex
: 'schedule',
832 text
: gettext('Next Run'),
833 dataIndex
: 'next-run',
835 renderer
: PVE
.Utils
.render_next_event
,
838 header
: gettext('Storage'),
841 dataIndex
: 'storage',
844 header
: gettext('Comment'),
845 dataIndex
: 'comment',
846 renderer
: Ext
.htmlEncode
,
847 sorter
: (a
, b
) => (a
.data
.comment
|| '').localeCompare(b
.data
.comment
|| ''),
851 header
: gettext('Retention'),
852 dataIndex
: 'prune-backups',
853 renderer
: v
=> v
? PVE
.Parser
.printPropertyString(v
) : gettext('Fallback from storage config'),
857 header
: gettext('Selection'),
861 renderer
: PVE
.Utils
.render_backup_selection
,
866 itemdblclick
: run_editor
,
873 Ext
.define('pve-cluster-backup', {
874 extend
: 'Ext.data.Model',
888 { name
: 'enabled', type
: 'boolean' },
889 { name
: 'all', type
: 'boolean' },