]> git.proxmox.com Git - proxmox-backup.git/blob - www/tape/ChangerStatus.js
ui: tape/ChangerStatus: do not show progress on drive clean
[proxmox-backup.git] / www / tape / ChangerStatus.js
1 Ext.define('pbs-slot-model', {
2 extend: 'Ext.data.Model',
3 fields: ['entry-id', 'label-text', 'is-labeled', ' model', 'name', 'vendor', 'serial', 'state', 'status', 'pool',
4 {
5 name: 'is-blocked',
6 calculate: function(data) {
7 return data.state !== undefined;
8 },
9 },
10 ],
11 idProperty: 'entry-id',
12 });
13
14 Ext.define('PBS.TapeManagement.ChangerStatus', {
15 extend: 'Ext.panel.Panel',
16 alias: 'widget.pbsChangerStatus',
17
18 viewModel: {
19 data: {
20 changer: '',
21 },
22
23 formulas: {
24 changerSelected: (get) => get('changer') !== '',
25 },
26 },
27
28 controller: {
29 xclass: 'Ext.app.ViewController',
30
31 changerChange: function(field, value) {
32 let me = this;
33 let view = me.getView();
34 let vm = me.getViewModel();
35 vm.set('changer', value);
36 if (view.rendered) {
37 me.reload();
38 }
39 },
40
41 importTape: function(view, rI, cI, button, el, record) {
42 let me = this;
43 let vm = me.getViewModel();
44 let from = record.data['entry-id'];
45 let changer = encodeURIComponent(vm.get('changer'));
46 Ext.create('Proxmox.window.Edit', {
47 title: gettext('Import'),
48 isCreate: true,
49 submitText: gettext('OK'),
50 method: 'POST',
51 url: `/api2/extjs/tape/changer/${changer}/transfer`,
52 items: [
53 {
54 xtype: 'displayfield',
55 name: 'from',
56 value: from,
57 submitValue: true,
58 fieldLabel: gettext('From Slot'),
59 },
60 {
61 xtype: 'proxmoxintegerfield',
62 name: 'to',
63 fieldLabel: gettext('To Slot'),
64 },
65 ],
66 listeners: {
67 destroy: function() {
68 me.reload();
69 },
70 },
71 }).show();
72 },
73
74 slotTransfer: function(view, rI, cI, button, el, record) {
75 let me = this;
76 let vm = me.getViewModel();
77 let from = record.data['entry-id'];
78 let changer = encodeURIComponent(vm.get('changer'));
79 Ext.create('Proxmox.window.Edit', {
80 title: gettext('Transfer'),
81 isCreate: true,
82 submitText: gettext('OK'),
83 method: 'POST',
84 url: `/api2/extjs/tape/changer/${changer}/transfer`,
85 items: [
86 {
87 xtype: 'displayfield',
88 name: 'from',
89 value: from,
90 submitValue: true,
91 fieldLabel: gettext('From Slot'),
92 },
93 {
94 xtype: 'proxmoxintegerfield',
95 name: 'to',
96 fieldLabel: gettext('To Slot'),
97 },
98 ],
99 listeners: {
100 destroy: function() {
101 me.reload();
102 },
103 },
104 }).show();
105 },
106
107 erase: function(view, rI, cI, button, el, record) {
108 let me = this;
109 let vm = me.getViewModel();
110 let label = record.data['label-text'];
111
112 let changer = vm.get('changer');
113 Ext.create('PBS.TapeManagement.EraseWindow', {
114 label,
115 changer,
116 listeners: {
117 destroy: function() {
118 me.reload();
119 },
120 },
121 }).show();
122 },
123
124 load: function(view, rI, cI, button, el, record) {
125 let me = this;
126 let vm = me.getViewModel();
127 let label = record.data['label-text'];
128
129 let changer = vm.get('changer');
130
131 Ext.create('Proxmox.window.Edit', {
132 isCreate: true,
133 autoShow: true,
134 submitText: gettext('OK'),
135 title: gettext('Load Media into Drive'),
136 url: `/api2/extjs/tape/drive`,
137 method: 'POST',
138 submitUrl: function(url, values) {
139 let drive = values.drive;
140 delete values.drive;
141 return `${url}/${encodeURIComponent(drive)}/load-media`;
142 },
143 items: [
144 {
145 xtype: 'displayfield',
146 name: 'label-text',
147 value: label,
148 submitValue: true,
149 fieldLabel: gettext('Media'),
150 },
151 {
152 xtype: 'pbsDriveSelector',
153 fieldLabel: gettext('Drive'),
154 changer: changer,
155 name: 'drive',
156 },
157 ],
158 listeners: {
159 destroy: function() {
160 me.reload();
161 },
162 },
163 });
164 },
165
166 unload: async function(view, rI, cI, button, el, record) {
167 let me = this;
168 let drive = record.data.name;
169 try {
170 await PBS.Async.api2({
171 method: 'POST',
172 timeout: 5*60*1000,
173 url: `/api2/extjs/tape/drive/${encodeURIComponent(drive)}/unload`,
174 });
175 } catch (error) {
176 Ext.Msg.alert(gettext('Error'), error);
177 }
178 me.reload();
179 },
180
181 driveCommand: function(driveid, command, callback, params, method) {
182 let me = this;
183 let view = me.getView();
184 params = params || {};
185 method = method || 'GET';
186 Proxmox.Utils.API2Request({
187 url: `/api2/extjs/tape/drive/${driveid}/${command}`,
188 timeout: 5*60*1000,
189 method,
190 waitMsgTarget: view,
191 params,
192 success: function(response) {
193 callback(response);
194 },
195 failure: function(response) {
196 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
197 },
198 });
199 },
200
201 cartridgeMemory: function(view, rI, cI, button, el, record) {
202 let me = this;
203 let drive = record.data.name;
204 me.driveCommand(drive, 'cartridge-memory', function(response) {
205 Ext.create('Ext.window.Window', {
206 title: gettext('Cartridge Memory'),
207 modal: true,
208 width: 600,
209 height: 450,
210 layout: 'fit',
211 scrollable: true,
212 items: [
213 {
214 xtype: 'grid',
215 store: {
216 data: response.result.data,
217 },
218 columns: [
219 {
220 text: gettext('ID'),
221 dataIndex: 'id',
222 width: 60,
223 },
224 {
225 text: gettext('Name'),
226 dataIndex: 'name',
227 flex: 2,
228 },
229 {
230 text: gettext('Value'),
231 dataIndex: 'value',
232 flex: 1,
233 },
234 ],
235 },
236 ],
237 }).show();
238 });
239 },
240
241 cleanDrive: function(view, rI, cI, button, el, record) {
242 let me = this;
243 me.driveCommand(record.data.name, 'clean', function(response) {
244 me.reload();
245 }, {}, 'PUT');
246 },
247
248 volumeStatistics: function(view, rI, cI, button, el, record) {
249 let me = this;
250 let drive = record.data.name;
251 me.driveCommand(drive, 'volume-statistics', function(response) {
252 Ext.create('Ext.window.Window', {
253 title: gettext('Volume Statistics'),
254 modal: true,
255 width: 600,
256 height: 450,
257 layout: 'fit',
258 scrollable: true,
259 items: [
260 {
261 xtype: 'grid',
262 store: {
263 data: response.result.data,
264 },
265 columns: [
266 {
267 text: gettext('ID'),
268 dataIndex: 'id',
269 width: 60,
270 },
271 {
272 text: gettext('Name'),
273 dataIndex: 'name',
274 flex: 2,
275 },
276 {
277 text: gettext('Value'),
278 dataIndex: 'value',
279 flex: 1,
280 },
281 ],
282 },
283 ],
284 }).show();
285 });
286 },
287
288 readLabel: function(view, rI, cI, button, el, record) {
289 let me = this;
290 let drive = record.data.name;
291 me.driveCommand(drive, 'read-label', function(response) {
292 let lines = [];
293 for (const [key, val] of Object.entries(response.result.data)) {
294 lines.push(`${key}: ${val}`);
295 }
296
297 let txt = lines.join('<br>');
298
299 Ext.Msg.show({
300 title: gettext('Label Information'),
301 message: txt,
302 icon: undefined,
303 });
304 });
305 },
306
307 status: function(view, rI, cI, button, el, record) {
308 let me = this;
309 let drive = record.data.name;
310 me.driveCommand(drive, 'status', function(response) {
311 let lines = [];
312 for (const [key, val] of Object.entries(response.result.data)) {
313 lines.push(`${key}: ${val}`);
314 }
315
316 let txt = lines.join('<br>');
317
318 Ext.Msg.show({
319 title: gettext('Status'),
320 message: txt,
321 icon: undefined,
322 });
323 });
324 },
325
326 reloadList: function() {
327 let me = this;
328 me.lookup('changerselector').getStore().load();
329 },
330
331 barcodeLabel: function() {
332 let me = this;
333 let vm = me.getViewModel();
334 let changer = vm.get('changer');
335 if (changer === '') {
336 return;
337 }
338
339 Ext.create('Proxmox.window.Edit', {
340 title: gettext('Barcode Label'),
341 showTaskViewer: true,
342 method: 'POST',
343 url: '/api2/extjs/tape/drive',
344 submitUrl: function(url, values) {
345 let drive = values.drive;
346 delete values.drive;
347 return `${url}/${encodeURIComponent(drive)}/barcode-label-media`;
348 },
349
350 items: [
351 {
352 xtype: 'pbsDriveSelector',
353 fieldLabel: gettext('Drive'),
354 name: 'drive',
355 changer: changer,
356 },
357 {
358 xtype: 'pbsMediaPoolSelector',
359 fieldLabel: gettext('Pool'),
360 name: 'pool',
361 skipEmptyText: true,
362 allowBlank: true,
363 },
364 ],
365 }).show();
366 },
367
368 inventory: function() {
369 let me = this;
370 let vm = me.getViewModel();
371 let changer = vm.get('changer');
372 if (changer === '') {
373 return;
374 }
375
376 Ext.create('Proxmox.window.Edit', {
377 title: gettext('Inventory'),
378 showTaskViewer: true,
379 method: 'PUT',
380 url: '/api2/extjs/tape/drive',
381 submitUrl: function(url, values) {
382 let drive = values.drive;
383 delete values.drive;
384 return `${url}/${encodeURIComponent(drive)}/inventory`;
385 },
386
387 items: [
388 {
389 xtype: 'pbsDriveSelector',
390 fieldLabel: gettext('Drive'),
391 name: 'drive',
392 changer: changer,
393 },
394 ],
395 }).show();
396 },
397
398 scheduleReload: function(time) {
399 let me = this;
400 if (me.reloadTimeout === undefined) {
401 me.reloadTimeout = setTimeout(function() {
402 me.reload();
403 }, time);
404 }
405 },
406
407 reload: function() {
408 let me = this;
409 if (me.reloadTimeout !== undefined) {
410 clearTimeout(me.reloadTimeout);
411 me.reloadTimeout = undefined;
412 }
413 me.reload_full(true);
414 },
415
416 reload_no_cache: function() {
417 let me = this;
418 if (me.reloadTimeout !== undefined) {
419 clearTimeout(me.reloadTimeout);
420 me.reloadTimeout = undefined;
421 }
422 me.reload_full(false);
423 },
424
425 reload_full: async function(use_cache) {
426 let me = this;
427 let view = me.getView();
428 let vm = me.getViewModel();
429 let changer = vm.get('changer');
430 if (changer === '') {
431 return;
432 }
433
434 try {
435 if (!use_cache) {
436 Proxmox.Utils.setErrorMask(view, true);
437 Proxmox.Utils.setErrorMask(me.lookup('content'));
438 }
439 let status_fut = PBS.Async.api2({
440 timeout: 5*60*1000,
441 method: 'GET',
442 url: `/api2/extjs/tape/changer/${encodeURIComponent(changer)}/status`,
443 params: {
444 cache: use_cache,
445 },
446 });
447 let drives_fut = PBS.Async.api2({
448 timeout: 5*60*1000,
449 url: `/api2/extjs/tape/drive?changer=${encodeURIComponent(changer)}`,
450 });
451
452 let tapes_fut = PBS.Async.api2({
453 timeout: 5*60*1000,
454 url: '/api2/extjs/tape/media/list',
455 method: 'GET',
456 params: {
457 "update-status": false,
458 },
459 });
460
461 let [status, drives, tapes_list] = await Promise.all([status_fut, drives_fut, tapes_fut]);
462
463 let data = {
464 slot: [],
465 'import-export': [],
466 drive: [],
467 };
468
469 let tapes = {};
470
471 for (const tape of tapes_list.result.data) {
472 tapes[tape['label-text']] = {
473 labeled: true,
474 pool: tape.pool,
475 status: tape.expired ? 'expired' : tape.status,
476 };
477 }
478
479 let drive_entries = {};
480
481 for (const entry of drives.result.data) {
482 drive_entries[entry['changer-drivenum'] || 0] = entry;
483 }
484
485 for (let entry of status.result.data) {
486 let type = entry['entry-kind'];
487
488 if (type === 'drive' && drive_entries[entry['entry-id']] !== undefined) {
489 entry = Ext.applyIf(entry, drive_entries[entry['entry-id']]);
490 }
491
492 if (tapes[entry['label-text']] !== undefined) {
493 entry['is-labeled'] = true;
494 entry.pool = tapes[entry['label-text']].pool;
495 entry.status = tapes[entry['label-text']].status;
496 } else {
497 entry['is-labeled'] = false;
498 }
499
500 data[type].push(entry);
501 }
502
503 // the stores are diffstores and are only refreshed
504 // on a 'load' event, which does not trigger on 'setData'
505 // so we have to fire them ourselves
506
507 me.lookup('slots').getStore().rstore.setData(data.slot);
508 me.lookup('slots').getStore().rstore.fireEvent('load', me, [], true);
509
510 me.lookup('import_export').getStore().rstore.setData(data['import-export']);
511 me.lookup('import_export').getStore().rstore.fireEvent('load', me, [], true);
512
513 me.lookup('drives').getStore().rstore.setData(data.drive);
514 me.lookup('drives').getStore().rstore.fireEvent('load', me, [], true);
515
516 if (!use_cache) {
517 Proxmox.Utils.setErrorMask(view);
518 }
519 Proxmox.Utils.setErrorMask(me.lookup('content'));
520 } catch (err) {
521 if (!use_cache) {
522 Proxmox.Utils.setErrorMask(view);
523 }
524 Proxmox.Utils.setErrorMask(me.lookup('content'), err.toString());
525 }
526
527 me.scheduleReload(5000);
528 },
529
530 renderIsLabeled: function(value, mD, record) {
531 if (!record.data['label-text']) {
532 return "";
533 }
534
535 if (record.data['label-text'].startsWith("CLN")) {
536 return "";
537 }
538
539 if (!value) {
540 return gettext('Not Labeled');
541 }
542
543 let status = record.data.status;
544 if (record.data.pool) {
545 return `${status} (${record.data.pool})`;
546 }
547 return status;
548 },
549
550 renderState: function(value, md, record) {
551 if (!value) {
552 return gettext('Idle');
553 }
554
555 let icon = '<i class="fa fa-spinner fa-pulse fa-fw"></i>';
556
557 if (value.startsWith("UPID")) {
558 let upid = Proxmox.Utils.parse_task_upid(value);
559 md.tdCls = "pointer";
560 return `${icon} ${upid.desc}`;
561 }
562
563 return `${icon} ${value}`;
564 },
565
566 control: {
567 'grid[reference=drives]': {
568 cellclick: function(table, td, ci, rec, tr, ri, e) {
569 if (e.position.column.dataIndex !== 'state') {
570 return;
571 }
572
573 let upid = rec.data.state;
574 if (!upid || !upid.startsWith("UPID")) {
575 return;
576 }
577
578 Ext.create('Proxmox.window.TaskViewer', {
579 autoShow: true,
580 upid,
581 });
582 },
583 },
584 },
585 },
586
587 listeners: {
588 activate: 'reload',
589 },
590
591 tbar: [
592 {
593 fieldLabel: gettext('Changer'),
594 xtype: 'pbsChangerSelector',
595 reference: 'changerselector',
596 autoSelect: true,
597 listeners: {
598 change: 'changerChange',
599 },
600 },
601 '-',
602 {
603 text: gettext('Reload'),
604 xtype: 'proxmoxButton',
605 handler: 'reload_no_cache',
606 selModel: false,
607 },
608 '-',
609 {
610 text: gettext('Barcode Label'),
611 xtype: 'proxmoxButton',
612 handler: 'barcodeLabel',
613 iconCls: 'fa fa-barcode',
614 bind: {
615 disabled: '{!changerSelected}',
616 },
617 },
618 {
619 text: gettext('Inventory'),
620 xtype: 'proxmoxButton',
621 handler: 'inventory',
622 iconCls: 'fa fa-book',
623 bind: {
624 disabled: '{!changerSelected}',
625 },
626 },
627 ],
628
629 layout: 'auto',
630 bodyPadding: 5,
631 scrollable: true,
632
633 items: [
634 {
635 xtype: 'container',
636 reference: 'content',
637 layout: {
638 type: 'hbox',
639 aling: 'stretch',
640 },
641 items: [
642 {
643 xtype: 'grid',
644 reference: 'slots',
645 title: gettext('Slots'),
646 padding: 5,
647 flex: 1,
648 store: {
649 type: 'diff',
650 rstore: {
651 type: 'store',
652 model: 'pbs-slot-model',
653 },
654 data: [],
655 },
656 columns: [
657 {
658 text: gettext('ID'),
659 dataIndex: 'entry-id',
660 width: 50,
661 },
662 {
663 text: gettext("Content"),
664 dataIndex: 'label-text',
665 flex: 1,
666 renderer: (value) => value || '',
667 },
668 {
669 text: gettext('Inventory'),
670 dataIndex: 'is-labeled',
671 renderer: 'renderIsLabeled',
672 flex: 1,
673 },
674 {
675 text: gettext('Actions'),
676 xtype: 'actioncolumn',
677 width: 100,
678 items: [
679 {
680 iconCls: 'fa fa-rotate-90 fa-exchange',
681 handler: 'slotTransfer',
682 tooltip: gettext('Transfer'),
683 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
684 },
685 {
686 iconCls: 'fa fa-trash-o',
687 handler: 'erase',
688 tooltip: gettext('Erase'),
689 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
690 },
691 {
692 iconCls: 'fa fa-rotate-90 fa-upload',
693 handler: 'load',
694 tooltip: gettext('Load'),
695 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
696 },
697 ],
698 },
699 ],
700 },
701 {
702 xtype: 'container',
703 flex: 2,
704 defaults: {
705 padding: 5,
706 },
707 items: [
708 {
709 xtype: 'grid',
710 reference: 'drives',
711 title: gettext('Drives'),
712 store: {
713 type: 'diff',
714 rstore: {
715 type: 'store',
716 model: 'pbs-slot-model',
717 },
718 data: [],
719 },
720 columns: [
721 {
722 text: gettext('ID'),
723 dataIndex: 'entry-id',
724 hidden: true,
725 width: 50,
726 },
727 {
728 text: gettext("Content"),
729 dataIndex: 'label-text',
730 flex: 1,
731 renderer: (value) => value || '',
732 },
733 {
734 text: gettext('Inventory'),
735 dataIndex: 'is-labeled',
736 renderer: 'renderIsLabeled',
737 flex: 1.5,
738 },
739 {
740 text: gettext("Name"),
741 sortable: true,
742 dataIndex: 'name',
743 flex: 1,
744 renderer: Ext.htmlEncode,
745 },
746 {
747 text: gettext('State'),
748 dataIndex: 'state',
749 flex: 3,
750 renderer: 'renderState',
751 },
752 {
753 text: gettext("Vendor"),
754 sortable: true,
755 dataIndex: 'vendor',
756 hidden: true,
757 flex: 1,
758 renderer: Ext.htmlEncode,
759 },
760 {
761 text: gettext("Model"),
762 sortable: true,
763 dataIndex: 'model',
764 hidden: true,
765 flex: 1,
766 renderer: Ext.htmlEncode,
767 },
768 {
769 text: gettext("Serial"),
770 sortable: true,
771 dataIndex: 'serial',
772 hidden: true,
773 flex: 1,
774 renderer: Ext.htmlEncode,
775 },
776 {
777 xtype: 'actioncolumn',
778 text: gettext('Actions'),
779 width: 140,
780 items: [
781 {
782 iconCls: 'fa fa-rotate-270 fa-upload',
783 handler: 'unload',
784 tooltip: gettext('Unload'),
785 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'] || rec.data['is-blocked'],
786 },
787 {
788 iconCls: 'fa fa-hdd-o',
789 handler: 'cartridgeMemory',
790 tooltip: gettext('Cartridge Memory'),
791 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'] || rec.data['is-blocked'],
792 },
793 {
794 iconCls: 'fa fa-line-chart',
795 handler: 'volumeStatistics',
796 tooltip: gettext('Volume Statistics'),
797 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'] || rec.data['is-blocked'],
798 },
799 {
800 iconCls: 'fa fa-tag',
801 handler: 'readLabel',
802 tooltip: gettext('Read Label'),
803 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'] || rec.data['is-blocked'],
804 },
805 {
806 iconCls: 'fa fa-info-circle',
807 tooltip: gettext('Status'),
808 handler: 'status',
809 isDisabled: (v, r, c, i, rec) => rec.data['is-blocked'],
810 },
811 {
812 iconCls: 'fa fa-shower',
813 tooltip: gettext('Clean Drive'),
814 handler: 'cleanDrive',
815 isDisabled: (v, r, c, i, rec) => rec.data['is-blocked'],
816 },
817 ],
818 },
819 ],
820 },
821 {
822 xtype: 'grid',
823 reference: 'import_export',
824 store: {
825 type: 'diff',
826 rstore: {
827 type: 'store',
828 model: 'pbs-slot-model',
829 },
830 data: [],
831 },
832 title: gettext('Import-Export Slots'),
833 columns: [
834 {
835 text: gettext('ID'),
836 dataIndex: 'entry-id',
837 width: 50,
838 },
839 {
840 text: gettext("Content"),
841 dataIndex: 'label-text',
842 renderer: (value) => value || '',
843 flex: 1,
844 },
845 {
846 text: gettext('Inventory'),
847 dataIndex: 'is-labeled',
848 renderer: 'renderIsLabeled',
849 flex: 1,
850 },
851 {
852 text: gettext('Actions'),
853 xtype: 'actioncolumn',
854 items: [
855 {
856 iconCls: 'fa fa-rotate-270 fa-upload',
857 handler: 'importTape',
858 tooltip: gettext('Import'),
859 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
860 },
861 ],
862 width: 80,
863 },
864 ],
865 },
866 ],
867 },
868 ],
869 },
870 ],
871 });