]>
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 // 'value' can be assigned a string or an array
22 let selModeField
= Ext
.create('Proxmox.form.KVComboBox', {
23 xtype
: 'proxmoxKVComboBox',
25 ['include', gettext('Include selected VMs')],
26 ['all', gettext('All')],
27 ['exclude', gettext('Exclude selected VMs')],
28 ['pool', gettext('Pool based')],
30 fieldLabel
: gettext('Selection mode'),
36 let storagesel
= Ext
.create('PVE.form.StorageSelector', {
37 fieldLabel
: gettext('Storage'),
39 storageContent
: 'backup',
43 change: function(f
, v
) {
44 let store
= f
.getStore();
45 let rec
= store
.findRecord('storage', v
, 0, false, true, true);
46 let compressionSelector
= me
.down('pveCompressionSelector');
48 if (rec
&& rec
.data
&& rec
.data
.type
=== 'pbs') {
49 compressionSelector
.setValue('zstd');
50 compressionSelector
.setDisabled(true);
51 } else if (!compressionSelector
.getEditable()) {
52 compressionSelector
.setDisabled(false);
58 let vmgrid
= Ext
.createWidget('vmselector', {
63 columnSelection
: ['vmid', 'node', 'status', 'name', 'type'],
66 let selectPoolMembers = function(poolid
) {
70 vmgrid
.selModel
.deselectAll(true);
71 vmgrid
.getStore().filter([
78 vmgrid
.selModel
.selectAll(true);
81 let selPool
= Ext
.create('PVE.form.PoolSelector', {
82 fieldLabel
: gettext('Pool to backup'),
87 change: function(selpool
, newValue
, oldValue
) {
88 selectPoolMembers(newValue
);
93 let nodesel
= Ext
.create('PVE.form.NodeSelector', {
95 fieldLabel
: gettext('Node'),
99 emptyText
: '-- ' + gettext('All') + ' --',
101 change: function(f
, value
) {
102 storagesel
.setNodename(value
);
103 let mode
= selModeField
.getValue();
104 let store
= vmgrid
.getStore();
106 store
.filterBy(function(rec
) {
107 return !value
|| rec
.get('node') === value
;
109 if (mode
=== 'all') {
110 vmgrid
.selModel
.selectAll(true);
112 if (mode
=== 'pool') {
113 selectPoolMembers(selPool
.value
);
123 xtype
: 'pveCalendarEvent',
124 fieldLabel
: gettext('Schedule'),
135 fieldLabel
: gettext('Send email to'),
139 xtype
: 'pveEmailNotificationSelector',
140 fieldLabel
: gettext('Email'),
141 name
: 'mailnotification',
142 deleteEmpty
: !me
.isCreate
,
143 value
: me
.isCreate
? 'always' : '',
146 xtype
: 'pveCompressionSelector',
147 fieldLabel
: gettext('Compression'),
149 deleteEmpty
: !me
.isCreate
,
153 xtype
: 'pveBackupModeSelector',
154 fieldLabel
: gettext('Mode'),
159 xtype
: 'proxmoxcheckbox',
160 fieldLabel
: gettext('Enable'),
168 let ipanel
= Ext
.create('Proxmox.panel.InputPanel', {
169 onlineHelp
: 'chapter_vzdump',
174 xtype
: 'proxmoxtextfield',
176 fieldLabel
: gettext('Job Comment'),
177 deleteEmpty
: !me
.isCreate
,
180 'data-qtip': gettext('Description of the job'),
187 xtype
: 'proxmoxcheckbox',
188 fieldLabel
: gettext('Repeat missed'),
189 name
: 'repeat-missed',
192 deleteDefaultValue
: !me
.isCreate
,
195 onGetValues: function(values
) {
198 Proxmox
.Utils
.assemble_field_data(values
, { 'delete': 'node' });
203 if (!values
.id
&& me
.isCreate
) {
204 values
.id
= 'backup-' + Ext
.data
.identifier
.Uuid
.Global
.generate().slice(0, 13);
207 let selMode
= values
.selMode
;
208 delete values
.selMode
;
210 if (selMode
=== 'all') {
214 } else if (selMode
=== 'exclude') {
216 values
.exclude
= values
.vmid
;
218 } else if (selMode
=== 'pool') {
222 if (selMode
!== 'pool') {
229 selModeField
.on('change', function(f
, value
, oldValue
) {
230 if (oldValue
=== 'pool') {
231 vmgrid
.getStore().removeFilter('poolFilter');
234 if (oldValue
=== 'all' || oldValue
=== 'pool') {
235 vmgrid
.selModel
.deselectAll(true);
238 if (value
=== 'all') {
239 vmgrid
.selModel
.selectAll(true);
240 vmgrid
.setDisabled(true);
242 vmgrid
.setDisabled(false);
245 if (value
=== 'pool') {
246 vmgrid
.setDisabled(true);
247 vmgrid
.selModel
.deselectAll(true);
248 selPool
.setVisible(true);
249 selPool
.setDisabled(false);
250 selPool
.allowBlank
= false;
251 selectPoolMembers(selPool
.value
);
253 selPool
.setVisible(false);
254 selPool
.setDisabled(true);
255 selPool
.allowBlank
= true;
260 subject
: gettext("Backup Job"),
273 title
: gettext('General'),
284 xtype
: 'pveBackupJobPrunePanel',
285 title
: gettext('Retention'),
286 isCreate
: me
.isCreate
,
287 keepAllDefaultForCreate
: false,
289 fallbackHintHtml
: gettext('Without any keep option, the storage\'s configuration or node\'s vzdump.conf is used as fallback'),
293 title
: gettext('Note Template'),
299 onGetValues: function(values
) {
300 if (values
['notes-template']) {
301 values
['notes-template'] = PVE
.Utils
.escapeNotesTemplate(
302 values
['notes-template']);
309 name
: 'notes-template',
310 fieldLabel
: gettext('Backup Notes'),
313 deleteEmpty
: !me
.isCreate
,
314 value
: me
.isCreate
? '{{guestname}}' : undefined,
320 'line-height': '1.5em',
322 html
: gettext('The notes are added to each backup created by this job.')
325 gettext('Possible template variables are: {0}'),
326 PVE
.Utils
.notesTemplateVars
.map(v
=> `<code>{{${v}}}</code>`).join(', '),
340 selModeField
.setValue('include');
343 success: function(response
, options
) {
344 let data
= response
.result
.data
;
346 data
.dow
= (data
.dow
|| '').split(',');
348 if (data
.all
|| data
.exclude
) {
350 data
.vmid
= data
.exclude
;
351 data
.selMode
= 'exclude';
354 data
.selMode
= 'all';
356 } else if (data
.pool
) {
357 data
.selMode
= 'pool';
358 data
.selPool
= data
.pool
;
360 data
.selMode
= 'include';
363 if (data
['prune-backups']) {
364 Object
.assign(data
, data
['prune-backups']);
365 delete data
['prune-backups'];
366 } else if (data
.maxfiles
!== undefined) {
367 if (data
.maxfiles
> 0) {
368 data
['keep-last'] = data
.maxfiles
;
370 data
['keep-all'] = 1;
372 delete data
.maxfiles
;
375 if (data
['notes-template']) {
376 data
['notes-template'] = PVE
.Utils
.unEscapeNotesTemplate(
377 data
['notes-template']);
387 Ext
.define('PVE.dc.BackupView', {
388 extend
: 'Ext.grid.GridPanel',
390 alias
: ['widget.pveDcBackupView'],
392 onlineHelp
: 'chapter_vzdump',
394 allText
: '-- ' + gettext('All') + ' --',
396 initComponent: function() {
399 let store
= new Ext
.data
.Store({
400 model
: 'pve-cluster-backup',
403 url
: "/api2/json/cluster/backup",
407 let not_backed_store
= new Ext
.data
.Store({
411 url
: 'api2/json/cluster/backup-info/not-backed-up',
415 let noBackupJobInfoButton
;
416 let reload = function() {
418 not_backed_store
.load({
419 callback
: records
=> noBackupJobInfoButton
.setVisible(records
.length
> 0),
423 let sm
= Ext
.create('Ext.selection.RowModel', {});
425 let run_editor = function() {
426 let rec
= sm
.getSelection()[0];
431 let win
= Ext
.create('PVE.dc.BackupEdit', {
434 win
.on('destroy', reload
);
438 let run_detail = function() {
439 let record
= sm
.getSelection()[0];
443 Ext
.create('Ext.window.Window', {
446 height
: Ext
.getBody().getViewSize().height
> 1000 ? 800 : 600, // factor out as common infra?
449 title
: gettext('Backup Details'),
460 xtype
: 'pveBackupInfo',
466 xtype
: 'pveBackupDiskTree',
467 title
: gettext('Included disks'),
469 jobid
: record
.data
.id
,
477 let run_backup_now = function(job
) {
478 job
= Ext
.clone(job
);
480 let jobNode
= job
.node
;
481 // Remove properties related to scheduling
483 delete job
.starttime
;
490 delete job
['next-run'];
491 delete job
['repeat-missed'];
492 job
.all
= job
.all
=== true ? 1 : 0;
494 ['performance', 'prune-backups'].forEach(key
=> {
496 job
[key
] = PVE
.Parser
.printPropertyString(job
[key
]);
500 let allNodes
= PVE
.data
.ResourceStore
.getNodes();
501 let nodes
= allNodes
.filter(node
=> node
.status
=== 'online').map(node
=> node
.node
);
504 if (jobNode
!== undefined) {
505 if (!nodes
.includes(jobNode
)) {
506 Ext
.Msg
.alert('Error', "Node '"+ jobNode
+"' from backup job isn't online!");
511 let unkownNodes
= allNodes
.filter(node
=> node
.status
!== 'online');
512 if (unkownNodes
.length
> 0) {errors
.push(unkownNodes
.map(node
=> node
.node
+ ": " + gettext("Node is offline")));}
514 let jobTotalCount
= nodes
.length
, jobsStarted
= 0;
517 title
: gettext('Please wait...'),
520 progressText
: '0/' + jobTotalCount
,
523 let postRequest = function() {
525 Ext
.Msg
.updateProgress(jobsStarted
/ jobTotalCount
, jobsStarted
+ '/' + jobTotalCount
);
527 if (jobsStarted
=== jobTotalCount
) {
529 if (errors
.length
> 0) {
530 Ext
.Msg
.alert('Error', 'Some errors have been encountered:<br />' + errors
.join('<br />'));
535 nodes
.forEach(node
=> Proxmox
.Utils
.API2Request({
536 url
: '/nodes/' + node
+ '/vzdump',
539 failure: function(response
, opts
) {
540 errors
.push(node
+ ': ' + response
.htmlStatus
);
543 success
: postRequest
,
547 var edit_btn
= new Proxmox
.button
.Button({
548 text
: gettext('Edit'),
554 var run_btn
= new Proxmox
.button
.Button({
555 text
: gettext('Run now'),
558 handler: function() {
559 var rec
= sm
.getSelection()[0];
565 title
: gettext('Confirm'),
566 icon
: Ext
.Msg
.QUESTION
,
567 msg
: gettext('Start the selected backup job now?'),
568 buttons
: Ext
.Msg
.YESNO
,
569 callback: function(btn
) {
573 run_backup_now(rec
.data
);
579 var remove_btn
= Ext
.create('Proxmox.button.StdRemoveButton', {
581 baseurl
: '/cluster/backup',
582 callback: function() {
587 var detail_btn
= new Proxmox
.button
.Button({
588 text
: gettext('Job Detail'),
590 tooltip
: gettext('Show job details and which guests and volumes are affected by the backup job'),
595 noBackupJobInfoButton
= new Proxmox
.button
.Button({
596 text
: `${gettext('Show')}: ${gettext('Guests Without Backup Job')}`,
597 tooltip
: gettext('Some guests are not covered by any backup job.'),
598 iconCls
: 'fa fa-fw fa-exclamation-circle',
601 Ext
.create('Ext.window.Window', {
608 title
: gettext('Guests Without Backup Job'),
619 xtype
: 'pveBackedGuests',
622 store
: not_backed_store
,
631 Proxmox
.Utils
.monStoreErrors(me
, store
);
637 stateId
: 'grid-dc-backup',
643 overflowHandler
: 'scroller',
647 text
: gettext('Add'),
648 handler: function() {
649 var win
= Ext
.create('PVE.dc.BackupEdit', {});
650 win
.on('destroy', reload
);
661 noBackupJobInfoButton
,
664 xtype
: 'proxmoxButton',
666 text
: gettext('Schedule Simulator'),
668 let record
= sm
.getSelection()[0];
671 schedule
= record
.data
.schedule
;
673 Ext
.create('PVE.window.ScheduleSimulator', {
683 header
: gettext('Enabled'),
685 dataIndex
: 'enabled',
686 xtype
: 'checkcolumn',
689 disabledCls
: 'x-item-enabled',
690 stopSelection
: false,
693 header
: gettext('ID'),
698 header
: gettext('Node'),
702 renderer: function(value
) {
710 header
: gettext('Schedule'),
712 dataIndex
: 'schedule',
715 text
: gettext('Next Run'),
716 dataIndex
: 'next-run',
718 renderer
: PVE
.Utils
.render_next_event
,
721 header
: gettext('Storage'),
724 dataIndex
: 'storage',
727 header
: gettext('Comment'),
728 dataIndex
: 'comment',
729 renderer
: Ext
.htmlEncode
,
730 sorter
: (a
, b
) => (a
.data
.comment
|| '').localeCompare(b
.data
.comment
|| ''),
734 header
: gettext('Retention'),
735 dataIndex
: 'prune-backups',
736 renderer
: v
=> v
? PVE
.Parser
.printPropertyString(v
) : gettext('Fallback from storage config'),
740 header
: gettext('Selection'),
744 renderer
: PVE
.Utils
.render_backup_selection
,
749 itemdblclick
: run_editor
,
756 Ext
.define('pve-cluster-backup', {
757 extend
: 'Ext.data.Model',
771 { name
: 'enabled', type
: 'boolean' },
772 { name
: 'all', type
: 'boolean' },