]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/dc/Backup.js
13cd0528c6f6b2855c88732f0bf5c4f577ee7954
[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 allExceptText: gettext('All except {0}'),
387
388 initComponent : function() {
389 var me = this;
390
391 var store = new Ext.data.Store({
392 model: 'pve-cluster-backup',
393 proxy: {
394 type: 'proxmox',
395 url: "/api2/json/cluster/backup"
396 }
397 });
398
399 var reload = function() {
400 store.load();
401 };
402
403 var sm = Ext.create('Ext.selection.RowModel', {});
404
405 var run_editor = function() {
406 var rec = sm.getSelection()[0];
407 if (!rec) {
408 return;
409 }
410
411 var win = Ext.create('PVE.dc.BackupEdit', {
412 jobid: rec.data.id
413 });
414 win.on('destroy', reload);
415 win.show();
416 };
417
418 var run_backup_now = function(job) {
419 job = Ext.clone(job);
420
421 let jobNode = job.node;
422 // Remove properties related to scheduling
423 delete job.enabled;
424 delete job.starttime;
425 delete job.dow;
426 delete job.id;
427 delete job.node;
428 job.all = job.all === true ? 1 : 0;
429
430 let allNodes = PVE.data.ResourceStore.getNodes();
431 let nodes = allNodes.filter(node => node.status === 'online').map(node => node.node);
432 let errors = [];
433
434 if (jobNode !== undefined) {
435 if (!nodes.includes(jobNode)) {
436 Ext.Msg.alert('Error', "Node '"+ jobNode +"' from backup job isn't online!");
437 return;
438 }
439 nodes = [ jobNode ];
440 } else {
441 let unkownNodes = allNodes.filter(node => node.status !== 'online');
442 if (unkownNodes.length > 0)
443 errors.push(unkownNodes.map(node => node.node + ": " + gettext("Node is offline")));
444 }
445 let jobTotalCount = nodes.length, jobsStarted = 0;
446
447 Ext.Msg.show({
448 title: gettext('Please wait...'),
449 closable: false,
450 progress: true,
451 progressText: '0/' + jobTotalCount,
452 });
453
454 let postRequest = function () {
455 jobsStarted++;
456 Ext.Msg.updateProgress(jobsStarted / jobTotalCount, jobsStarted + '/' + jobTotalCount);
457
458 if (jobsStarted == jobTotalCount) {
459 Ext.Msg.hide();
460 if (errors.length > 0) {
461 Ext.Msg.alert('Error', 'Some errors have been encountered:<br />' + errors.join('<br />'));
462 }
463 }
464 };
465
466 nodes.forEach(node => Proxmox.Utils.API2Request({
467 url: '/nodes/' + node + '/vzdump',
468 method: 'POST',
469 params: job,
470 failure: function (response, opts) {
471 errors.push(node + ': ' + response.htmlStatus);
472 postRequest();
473 },
474 success: postRequest
475 }));
476 };
477
478 var edit_btn = new Proxmox.button.Button({
479 text: gettext('Edit'),
480 disabled: true,
481 selModel: sm,
482 handler: run_editor
483 });
484
485 var run_btn = new Proxmox.button.Button({
486 text: gettext('Run now'),
487 disabled: true,
488 selModel: sm,
489 handler: function() {
490 var rec = sm.getSelection()[0];
491 if (!rec) {
492 return;
493 }
494
495 Ext.Msg.show({
496 title: gettext('Confirm'),
497 icon: Ext.Msg.QUESTION,
498 msg: gettext('Start the selected backup job now?'),
499 buttons: Ext.Msg.YESNO,
500 callback: function(btn) {
501 if (btn !== 'yes') {
502 return;
503 }
504 run_backup_now(rec.data);
505 }
506 });
507 }
508 });
509
510 var remove_btn = Ext.create('Proxmox.button.StdRemoveButton', {
511 selModel: sm,
512 baseurl: '/cluster/backup',
513 callback: function() {
514 reload();
515 }
516 });
517
518 Proxmox.Utils.monStoreErrors(me, store);
519
520 Ext.apply(me, {
521 store: store,
522 selModel: sm,
523 stateful: true,
524 stateId: 'grid-dc-backup',
525 viewConfig: {
526 trackOver: false
527 },
528 tbar: [
529 {
530 text: gettext('Add'),
531 handler: function() {
532 var win = Ext.create('PVE.dc.BackupEdit',{});
533 win.on('destroy', reload);
534 win.show();
535 }
536 },
537 '-',
538 remove_btn,
539 edit_btn,
540 '-',
541 run_btn
542 ],
543 columns: [
544 {
545 header: gettext('Enabled'),
546 width: 80,
547 dataIndex: 'enabled',
548 xtype: 'checkcolumn',
549 sortable: true,
550 disabled: true,
551 disabledCls: 'x-item-enabled',
552 stopSelection: false
553 },
554 {
555 header: gettext('Node'),
556 width: 100,
557 sortable: true,
558 dataIndex: 'node',
559 renderer: function(value) {
560 if (value) {
561 return value;
562 }
563 return me.allText;
564 }
565 },
566 {
567 header: gettext('Day of week'),
568 width: 200,
569 sortable: false,
570 dataIndex: 'dow',
571 renderer: function(val) {
572 var dows = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
573 var selected = [];
574 var cur = -1;
575 val.split(',').forEach(function(day){
576 cur++;
577 var dow = (dows.indexOf(day)+6)%7;
578 if (cur === dow) {
579 if (selected.length === 0 || selected[selected.length-1] === 0) {
580 selected.push(1);
581 } else {
582 selected[selected.length-1]++;
583 }
584 } else {
585 while (cur < dow) {
586 cur++;
587 selected.push(0);
588 }
589 selected.push(1);
590 }
591 });
592
593 cur = -1;
594 var days = [];
595 selected.forEach(function(item) {
596 cur++;
597 if (item > 2) {
598 days.push(Ext.Date.dayNames[(cur+1)] + '-' + Ext.Date.dayNames[(cur+item)%7]);
599 cur += item-1;
600 } else if (item == 2) {
601 days.push(Ext.Date.dayNames[cur+1]);
602 days.push(Ext.Date.dayNames[(cur+2)%7]);
603 cur++;
604 } else if (item == 1) {
605 days.push(Ext.Date.dayNames[(cur+1)%7]);
606 }
607 });
608 return days.join(', ');
609 }
610 },
611 {
612 header: gettext('Start Time'),
613 width: 60,
614 sortable: true,
615 dataIndex: 'starttime'
616 },
617 {
618 header: gettext('Storage'),
619 width: 100,
620 sortable: true,
621 dataIndex: 'storage'
622 },
623 {
624 header: gettext('Selection'),
625 flex: 1,
626 sortable: false,
627 dataIndex: 'vmid',
628 renderer: function(value, metaData, record) {
629 if (record.data.all) {
630 if (record.data.exclude) {
631 return Ext.String.format(me.allExceptText, record.data.exclude);
632 }
633 return me.allText;
634 }
635 if (record.data.vmid) {
636 return record.data.vmid;
637 }
638
639 if (record.data.pool) {
640 return "Pool '"+ record.data.pool + "'";
641 }
642
643 return "-";
644 }
645 }
646 ],
647 listeners: {
648 activate: reload,
649 itemdblclick: run_editor
650 }
651 });
652
653 me.callParent();
654 }
655 }, function() {
656
657 Ext.define('pve-cluster-backup', {
658 extend: 'Ext.data.Model',
659 fields: [
660 'id', 'starttime', 'dow',
661 'storage', 'node', 'vmid', 'exclude',
662 'mailto', 'pool', 'compress', 'mode',
663 { name: 'enabled', type: 'boolean' },
664 { name: 'all', type: 'boolean' }
665 ]
666 });
667 });