]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
1ef092c53a3e7a425aec553d422486004c2634f7
[pve-manager.git] / www / manager6 / dc / Backup.js
1 Ext.define('PVE.dc.BackupEdit', {
2 extend: 'Proxmox.window.Edit',
3 alias: ['widget.pveDcBackupEdit'],
4
5 defaultFocus: undefined,
6
7 initComponent : function() {
8 var me = this;
9
10 me.isCreate = !me.jobid;
11
12 var url;
13 var method;
14
15 if (me.isCreate) {
16 url = '/api2/extjs/cluster/backup';
17 method = 'POST';
18 } else {
19 url = '/api2/extjs/cluster/backup/' + me.jobid;
20 method = 'PUT';
21 }
22
23 var vmidField = Ext.create('Ext.form.field.Hidden', {
24 name: 'vmid'
25 });
26
27 // 'value' can be assigned a string or an array
28 var selModeField = Ext.create('Proxmox.form.KVComboBox', {
29 xtype: 'proxmoxKVComboBox',
30 comboItems: [
31 ['include', gettext('Include selected VMs')],
32 ['all', gettext('All')],
33 ['exclude', gettext('Exclude selected VMs')],
34 ['pool', gettext('Pool based')]
35 ],
36 fieldLabel: gettext('Selection mode'),
37 name: 'selMode',
38 value: ''
39 });
40
41 var sm = Ext.create('Ext.selection.CheckboxModel', {
42 mode: 'SIMPLE',
43 listeners: {
44 selectionchange: function(model, selected) {
45 var sel = [];
46 Ext.Array.each(selected, function(record) {
47 sel.push(record.data.vmid);
48 });
49
50 // to avoid endless recursion suspend the vmidField change
51 // event temporary as it calls us again
52 vmidField.suspendEvent('change');
53 vmidField.setValue(sel);
54 vmidField.resumeEvent('change');
55 }
56 }
57 });
58
59 var storagesel = Ext.create('PVE.form.StorageSelector', {
60 fieldLabel: gettext('Storage'),
61 nodename: 'localhost',
62 storageContent: 'backup',
63 allowBlank: false,
64 name: 'storage'
65 });
66
67 var store = new Ext.data.Store({
68 model: 'PVEResources',
69 sorters: {
70 property: 'vmid',
71 order: 'ASC'
72 }
73 });
74
75 var vmgrid = Ext.createWidget('grid', {
76 store: store,
77 border: true,
78 height: 300,
79 selModel: sm,
80 disabled: true,
81 columns: [
82 {
83 header: 'ID',
84 dataIndex: 'vmid',
85 width: 60
86 },
87 {
88 header: gettext('Node'),
89 dataIndex: 'node'
90 },
91 {
92 header: gettext('Status'),
93 dataIndex: 'uptime',
94 renderer: function(value) {
95 if (value) {
96 return Proxmox.Utils.runningText;
97 } else {
98 return Proxmox.Utils.stoppedText;
99 }
100 }
101 },
102 {
103 header: gettext('Name'),
104 dataIndex: 'name',
105 flex: 1
106 },
107 {
108 header: gettext('Type'),
109 dataIndex: 'type'
110 }
111 ]
112 });
113
114 var selectPoolMembers = function(poolid) {
115 if (!poolid) {
116 return;
117 }
118 sm.deselectAll(true);
119 store.filter([
120 {
121 id: 'poolFilter',
122 property: 'pool',
123 value: poolid
124 }
125 ]);
126 sm.selectAll(true);
127 };
128
129 var selPool = Ext.create('PVE.form.PoolSelector', {
130 fieldLabel: gettext('Pool to backup'),
131 hidden: true,
132 allowBlank: true,
133 name: 'pool',
134 listeners: {
135 change: function( selpool, newValue, oldValue) {
136 selectPoolMembers(newValue);
137 }
138 }
139 });
140
141 var nodesel = Ext.create('PVE.form.NodeSelector', {
142 name: 'node',
143 fieldLabel: gettext('Node'),
144 allowBlank: true,
145 editable: true,
146 autoSelect: false,
147 emptyText: '-- ' + gettext('All') + ' --',
148 listeners: {
149 change: function(f, value) {
150 storagesel.setNodename(value || 'localhost');
151 var mode = selModeField.getValue();
152 store.clearFilter();
153 store.filterBy(function(rec) {
154 return (!value || rec.get('node') === value);
155 });
156 if (mode === 'all') {
157 sm.selectAll(true);
158 }
159
160 if (mode === 'pool') {
161 selectPoolMembers(selPool.value);
162 }
163 }
164 }
165 });
166
167 var column1 = [
168 nodesel,
169 storagesel,
170 {
171 xtype: 'pveDayOfWeekSelector',
172 name: 'dow',
173 fieldLabel: gettext('Day of week'),
174 multiSelect: true,
175 value: ['sat'],
176 allowBlank: false
177 },
178 {
179 xtype: 'timefield',
180 fieldLabel: gettext('Start Time'),
181 name: 'starttime',
182 format: 'H:i',
183 formatText: 'HH:MM',
184 value: '00:00',
185 allowBlank: false
186 },
187 selModeField,
188 selPool
189 ];
190
191 var column2 = [
192 {
193 xtype: 'textfield',
194 fieldLabel: gettext('Send email to'),
195 name: 'mailto'
196 },
197 {
198 xtype: 'pveEmailNotificationSelector',
199 fieldLabel: gettext('Email notification'),
200 name: 'mailnotification',
201 deleteEmpty: me.isCreate ? false : true,
202 value: me.isCreate ? 'always' : ''
203 },
204 {
205 xtype: 'pveCompressionSelector',
206 fieldLabel: gettext('Compression'),
207 name: 'compress',
208 deleteEmpty: me.isCreate ? false : true,
209 value: 'zstd'
210 },
211 {
212 xtype: 'pveBackupModeSelector',
213 fieldLabel: gettext('Mode'),
214 value: 'snapshot',
215 name: 'mode'
216 },
217 {
218 xtype: 'proxmoxcheckbox',
219 fieldLabel: gettext('Enable'),
220 name: 'enabled',
221 uncheckedValue: 0,
222 defaultValue: 1,
223 checked: true
224 },
225 vmidField
226 ];
227
228 var ipanel = Ext.create('Proxmox.panel.InputPanel', {
229 onlineHelp: 'chapter_vzdump',
230 column1: column1,
231 column2: column2,
232 onGetValues: function(values) {
233 if (!values.node) {
234 if (!me.isCreate) {
235 Proxmox.Utils.assemble_field_data(values, { 'delete': 'node' });
236 }
237 delete values.node;
238 }
239
240 var selMode = values.selMode;
241 delete values.selMode;
242
243 if (selMode === 'all') {
244 values.all = 1;
245 values.exclude = '';
246 delete values.vmid;
247 } else if (selMode === 'exclude') {
248 values.all = 1;
249 values.exclude = values.vmid;
250 delete values.vmid;
251 } else if (selMode === 'pool') {
252 delete values.vmid;
253 }
254
255 if (selMode !== 'pool') {
256 delete values.pool;
257 }
258 return values;
259 }
260 });
261
262 var update_vmid_selection = function(list, mode) {
263 if (mode !== 'all' && mode !== 'pool') {
264 sm.deselectAll(true);
265 if (list) {
266 Ext.Array.each(list.split(','), function(vmid) {
267 var rec = store.findRecord('vmid', vmid);
268 if (rec) {
269 sm.select(rec, true);
270 }
271 });
272 }
273 }
274 };
275
276 vmidField.on('change', function(f, value) {
277 var mode = selModeField.getValue();
278 update_vmid_selection(value, mode);
279 });
280
281 selModeField.on('change', function(f, value, oldValue) {
282 if (oldValue === 'pool') {
283 store.removeFilter('poolFilter');
284 }
285
286 if (oldValue === 'all') {
287 sm.deselectAll(true);
288 vmidField.setValue('');
289 }
290
291 if (value === 'all') {
292 sm.selectAll(true);
293 vmgrid.setDisabled(true);
294 } else {
295 vmgrid.setDisabled(false);
296 }
297
298 if (value === 'pool') {
299 vmgrid.setDisabled(true);
300 vmidField.setValue('');
301 selPool.setVisible(true);
302 selPool.allowBlank = false;
303 selectPoolMembers(selPool.value);
304
305 } else {
306 selPool.setVisible(false);
307 selPool.allowBlank = true;
308 }
309 var list = vmidField.getValue();
310 update_vmid_selection(list, value);
311 });
312
313 var reload = function() {
314 store.load({
315 params: { type: 'vm' },
316 callback: function() {
317 var node = nodesel.getValue();
318 store.clearFilter();
319 store.filterBy(function(rec) {
320 return (!node || node.length === 0 || rec.get('node') === node);
321 });
322 var list = vmidField.getValue();
323 var mode = selModeField.getValue();
324 if (mode === 'all') {
325 sm.selectAll(true);
326 } else if (mode === 'pool'){
327 selectPoolMembers(selPool.value);
328 } else {
329 update_vmid_selection(list, mode);
330 }
331 }
332 });
333 };
334
335 Ext.applyIf(me, {
336 subject: gettext("Backup Job"),
337 url: url,
338 method: method,
339 items: [ ipanel, vmgrid ]
340 });
341
342 me.callParent();
343
344 if (me.isCreate) {
345 selModeField.setValue('include');
346 } else {
347 me.load({
348 success: function(response, options) {
349 var data = response.result.data;
350
351 data.dow = data.dow.split(',');
352
353 if (data.all || data.exclude) {
354 if (data.exclude) {
355 data.vmid = data.exclude;
356 data.selMode = 'exclude';
357 } else {
358 data.vmid = '';
359 data.selMode = 'all';
360 }
361 } else if (data.pool) {
362 data.selMode = 'pool';
363 data.selPool = data.pool;
364 } else {
365 data.selMode = 'include';
366 }
367
368 me.setValues(data);
369 }
370 });
371 }
372
373 reload();
374 }
375 });
376
377
378 Ext.define('PVE.dc.BackupView', {
379 extend: 'Ext.grid.GridPanel',
380
381 alias: ['widget.pveDcBackupView'],
382
383 onlineHelp: 'chapter_vzdump',
384
385 allText: '-- ' + gettext('All') + ' --',
386
387 initComponent : function() {
388 var me = this;
389
390 var store = new Ext.data.Store({
391 model: 'pve-cluster-backup',
392 proxy: {
393 type: 'proxmox',
394 url: "/api2/json/cluster/backup"
395 }
396 });
397
398 var reload = function() {
399 store.load();
400 };
401
402 var sm = Ext.create('Ext.selection.RowModel', {});
403
404 var run_editor = function() {
405 var rec = sm.getSelection()[0];
406 if (!rec) {
407 return;
408 }
409
410 var win = Ext.create('PVE.dc.BackupEdit', {
411 jobid: rec.data.id
412 });
413 win.on('destroy', reload);
414 win.show();
415 };
416
417 var run_backup_now = function(job) {
418 job = Ext.clone(job);
419
420 let jobNode = job.node;
421 // Remove properties related to scheduling
422 delete job.enabled;
423 delete job.starttime;
424 delete job.dow;
425 delete job.id;
426 delete job.node;
427 job.all = job.all === true ? 1 : 0;
428
429 let allNodes = PVE.data.ResourceStore.getNodes();
430 let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
431 let errors = [];
432
433 if (jobNode !== undefined) {
434 if (!nodes.includes(jobNode)) {
435 Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
436 return;
437 }
438 nodes = [ jobNode ];
439 } else {
440 let unkownNodes = allNodes.filter(node => node.status !== 'online');
441 if (unkownNodes.length > 0)
442 errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));
443 }
444 let jobTotalCount = nodes.length, jobsStarted = 0;
445
446 Ext.Msg.show({
447 title: gettext('Please wait...'),
448 closable: false,
449 progress: true,
450 progressText: '0/' + jobTotalCount,
451 });
452
453 let postRequest = function () {
454 jobsStarted++;
455 Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
456
457 if (jobsStarted == jobTotalCount) {
458 Ext.Msg.hide();
459 if (errors.length > 0) {
460 Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
461 }
462 }
463 };
464
465 nodes.forEach(node => Proxmox.Utils.API2Request({
466 url: '/nodes/' + node + '/vzdump',
467 method: 'POST',
468 params: job,
469 failure: function (response, opts) {
470 errors.push(node + ': ' + response.htmlStatus);
471 postRequest();
472 },
473 success: postRequest
474 }));
475 };
476
477 var edit_btn = new Proxmox.button.Button({
478 text: gettext('Edit'),
479 disabled: true,
480 selModel: sm,
481 handler: run_editor
482 });
483
484 var run_btn = new Proxmox.button.Button({
485 text: gettext('Run now'),
486 disabled: true,
487 selModel: sm,
488 handler: function() {
489 var rec = sm.getSelection()[0];
490 if (!rec) {
491 return;
492 }
493
494 Ext.Msg.show({
495 title: gettext('Confirm'),
496 icon: Ext.Msg.QUESTION,
497 msg: gettext('Start the selected backup job now?'),
498 buttons: Ext.Msg.YESNO,
499 callback: function(btn) {
500 if (btn !== 'yes') {
501 return;
502 }
503 run_backup_now(rec.data);
504 }
505 });
506 }
507 });
508
509 var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
510 selModel: sm,
511 baseurl: '/cluster/backup',
512 callback: function() {
513 reload();
514 }
515 });
516
517 Proxmox.Utils.monStoreErrors(me, store);
518
519 Ext.apply(me, {
520 store: store,
521 selModel: sm,
522 stateful: true,
523 stateId: 'grid-dc-backup',
524 viewConfig: {
525 trackOver: false
526 },
527 tbar: [
528 {
529 text: gettext('Add'),
530 handler: function() {
531 var win = Ext.create('PVE.dc.BackupEdit',{});
532 win.on('destroy', reload);
533 win.show();
534 }
535 },
536 '-',
537 remove_btn,
538 edit_btn,
539 '-',
540 run_btn
541 ],
542 columns: [
543 {
544 header: gettext('Enabled'),
545 width: 80,
546 dataIndex: 'enabled',
547 xtype: 'checkcolumn',
548 sortable: true,
549 disabled: true,
550 disabledCls: 'x-item-enabled',
551 stopSelection: false
552 },
553 {
554 header: gettext('Node'),
555 width: 100,
556 sortable: true,
557 dataIndex: 'node',
558 renderer: function(value) {
559 if (value) {
560 return value;
561 }
562 return me.allText;
563 }
564 },
565 {
566 header: gettext('Day of week'),
567 width: 200,
568 sortable: false,
569 dataIndex: 'dow',
570 renderer: PVE.Utils.render_backup_days_of_week
571 },
572 {
573 header: gettext('Start Time'),
574 width: 60,
575 sortable: true,
576 dataIndex: 'starttime'
577 },
578 {
579 header: gettext('Storage'),
580 width: 100,
581 sortable: true,
582 dataIndex: 'storage'
583 },
584 {
585 header: gettext('Selection'),
586 flex: 1,
587 sortable: false,
588 dataIndex: 'vmid',
589 renderer: PVE.Utils.render_backup_selection
590 }
591 ],
592 listeners: {
593 activate: reload,
594 itemdblclick: run_editor
595 }
596 });
597
598 me.callParent();
599 }
600 }, function() {
601
602 Ext.define('pve-cluster-backup', {
603 extend: 'Ext.data.Model',
604 fields: [
605 'id', 'starttime', 'dow',
606 'storage', 'node', 'vmid', 'exclude',
607 'mailto', 'pool', 'compress', 'mode',
608 { name: 'enabled', type: 'boolean' },
609 { name: 'all', type: 'boolean' }
610 ]
611 });
612 });