]> git.proxmox.com Git - proxmox-backup.git/blob - www/tape/ChangerStatus.js
tape: update changer status cache after load/unload
[proxmox-backup.git] / www / tape / ChangerStatus.js
1 Ext.define('PBS.TapeManagement.ChangerStatus', {
2 extend: 'Ext.panel.Panel',
3 alias: 'widget.pbsChangerStatus',
4
5 viewModel: {
6 data: {
7 changer: '',
8 },
9
10 formulas: {
11 changerSelected: (get) => get('changer') !== '',
12 },
13 },
14
15 controller: {
16 xclass: 'Ext.app.ViewController',
17
18 changerChange: function(field, value) {
19 let me = this;
20 let view = me.getView();
21 let vm = me.getViewModel();
22 vm.set('changer', value);
23 if (view.rendered) {
24 me.reload();
25 }
26 },
27
28 importTape: function(view, rI, cI, button, el, record) {
29 let me = this;
30 let vm = me.getViewModel();
31 let from = record.data['entry-id'];
32 let changer = encodeURIComponent(vm.get('changer'));
33 Ext.create('Proxmox.window.Edit', {
34 title: gettext('Import'),
35 isCreate: true,
36 submitText: gettext('OK'),
37 method: 'POST',
38 url: `/api2/extjs/tape/changer/${changer}/transfer`,
39 items: [
40 {
41 xtype: 'displayfield',
42 name: 'from',
43 value: from,
44 submitValue: true,
45 fieldLabel: gettext('From Slot'),
46 },
47 {
48 xtype: 'proxmoxintegerfield',
49 name: 'to',
50 fieldLabel: gettext('To Slot'),
51 },
52 ],
53 listeners: {
54 destroy: function() {
55 me.reload();
56 },
57 },
58 }).show();
59 },
60
61 slotTransfer: function(view, rI, cI, button, el, record) {
62 let me = this;
63 let vm = me.getViewModel();
64 let from = record.data['entry-id'];
65 let changer = encodeURIComponent(vm.get('changer'));
66 Ext.create('Proxmox.window.Edit', {
67 title: gettext('Transfer'),
68 isCreate: true,
69 submitText: gettext('OK'),
70 method: 'POST',
71 url: `/api2/extjs/tape/changer/${changer}/transfer`,
72 items: [
73 {
74 xtype: 'displayfield',
75 name: 'from',
76 value: from,
77 submitValue: true,
78 fieldLabel: gettext('From Slot'),
79 },
80 {
81 xtype: 'proxmoxintegerfield',
82 name: 'to',
83 fieldLabel: gettext('To Slot'),
84 },
85 ],
86 listeners: {
87 destroy: function() {
88 me.reload();
89 },
90 },
91 }).show();
92 },
93
94 erase: function(view, rI, cI, button, el, record) {
95 let me = this;
96 let vm = me.getViewModel();
97 let label = record.data['label-text'];
98
99 let changer = vm.get('changer');
100 Ext.create('PBS.TapeManagement.EraseWindow', {
101 label,
102 changer,
103 listeners: {
104 destroy: function() {
105 me.reload();
106 },
107 },
108 }).show();
109 },
110
111 load: function(view, rI, cI, button, el, record) {
112 let me = this;
113 let vm = me.getViewModel();
114 let label = record.data['label-text'];
115
116 let changer = vm.get('changer');
117
118 Ext.create('Proxmox.window.Edit', {
119 isCreate: true,
120 autoShow: true,
121 submitText: gettext('OK'),
122 title: gettext('Load Media into Drive'),
123 url: `/api2/extjs/tape/drive`,
124 showProgress: true,
125 method: 'POST',
126 submitUrl: function(url, values) {
127 let drive = values.drive;
128 delete values.drive;
129 return `${url}/${encodeURIComponent(drive)}/load-media`;
130 },
131 items: [
132 {
133 xtype: 'displayfield',
134 name: 'label-text',
135 value: label,
136 submitValue: true,
137 fieldLabel: gettext('Media'),
138 },
139 {
140 xtype: 'pbsDriveSelector',
141 fieldLabel: gettext('Drive'),
142 changer: changer,
143 name: 'drive',
144 },
145 ],
146 listeners: {
147 destroy: function() {
148 me.reload();
149 },
150 },
151 });
152 },
153
154 unload: async function(view, rI, cI, button, el, record) {
155 let me = this;
156 let drive = record.data.name;
157 try {
158 let response = await PBS.Async.api2({
159 method: 'POST',
160 timeout: 5*60*1000,
161 url: `/api2/extjs/tape/drive/${encodeURIComponent(drive)}/unload`,
162 });
163
164 Ext.create('Proxmox.window.TaskProgress', {
165 autoShow: true,
166 upid: response.result.data,
167 taskDone: () => me.reload(),
168 });
169 } catch (error) {
170 Ext.Msg.alert(gettext('Error'), error);
171 me.reload();
172 }
173 },
174
175 driveCommand: function(driveid, command, callback, params, method) {
176 let me = this;
177 let view = me.getView();
178 params = params || {};
179 method = method || 'GET';
180 Proxmox.Utils.API2Request({
181 url: `/api2/extjs/tape/drive/${driveid}/${command}`,
182 timeout: 5*60*1000,
183 method,
184 waitMsgTarget: view,
185 params,
186 success: function(response) {
187 callback(response);
188 },
189 failure: function(response) {
190 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
191 },
192 });
193 },
194
195 cartridgeMemory: function(view, rI, cI, button, el, record) {
196 let me = this;
197 let drive = record.data.name;
198 me.driveCommand(drive, 'cartridge-memory', function(response) {
199 Ext.create('Ext.window.Window', {
200 title: gettext('Cartridge Memory'),
201 modal: true,
202 width: 600,
203 height: 450,
204 layout: 'fit',
205 scrollable: true,
206 items: [
207 {
208 xtype: 'grid',
209 store: {
210 data: response.result.data,
211 },
212 columns: [
213 {
214 text: gettext('ID'),
215 dataIndex: 'id',
216 width: 60,
217 },
218 {
219 text: gettext('Name'),
220 dataIndex: 'name',
221 flex: 2,
222 },
223 {
224 text: gettext('Value'),
225 dataIndex: 'value',
226 flex: 1,
227 },
228 ],
229 },
230 ],
231 }).show();
232 });
233 },
234
235 cleanDrive: function(view, rI, cI, button, el, record) {
236 let me = this;
237 let drive = record.data.name;
238 me.driveCommand(drive, 'clean', function(response) {
239 Ext.create('Proxmox.window.TaskProgress', {
240 upid: response.result.data,
241 taskDone: function() {
242 me.reload();
243 },
244 }).show();
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 reload: function() {
399 this.reload_full(true);
400 },
401
402 reload_no_cache: function() {
403 this.reload_full(false);
404 },
405
406 reload_full: async function(use_cache) {
407 let me = this;
408 let view = me.getView();
409 let vm = me.getViewModel();
410 let changer = vm.get('changer');
411 if (changer === '') {
412 return;
413 }
414
415 try {
416 Proxmox.Utils.setErrorMask(view, true);
417 Proxmox.Utils.setErrorMask(me.lookup('content'));
418 let status_fut = PBS.Async.api2({
419 timeout: 5*60*1000,
420 method: 'GET',
421 url: `/api2/extjs/tape/changer/${encodeURIComponent(changer)}/status`,
422 params: {
423 cache: use_cache,
424 },
425 });
426 let drives_fut = PBS.Async.api2({
427 timeout: 5*60*1000,
428 url: `/api2/extjs/tape/drive?changer=${encodeURIComponent(changer)}`,
429 });
430
431 let tapes_fut = PBS.Async.api2({
432 timeout: 5*60*1000,
433 url: '/api2/extjs/tape/media/list',
434 method: 'GET',
435 params: {
436 "update-status": false,
437 },
438 });
439
440 let [status, drives, tapes_list] = await Promise.all([status_fut, drives_fut, tapes_fut]);
441
442 let data = {
443 slot: [],
444 'import-export': [],
445 drive: [],
446 };
447
448 let tapes = {};
449
450 for (const tape of tapes_list.result.data) {
451 tapes[tape['label-text']] = {
452 labeled: true,
453 pool: tape.pool,
454 status: tape.expired ? 'expired' : tape.status,
455 };
456 }
457
458 let drive_entries = {};
459
460 for (const entry of drives.result.data) {
461 drive_entries[entry['changer-drivenum'] || 0] = entry;
462 }
463
464 for (let entry of status.result.data) {
465 let type = entry['entry-kind'];
466
467 if (type === 'drive' && drive_entries[entry['entry-id']] !== undefined) {
468 entry = Ext.applyIf(entry, drive_entries[entry['entry-id']]);
469 }
470
471 if (tapes[entry['label-text']] !== undefined) {
472 entry['is-labeled'] = true;
473 entry.pool = tapes[entry['label-text']].pool;
474 entry.status = tapes[entry['label-text']].status;
475 } else {
476 entry['is-labeled'] = false;
477 }
478
479 data[type].push(entry);
480 }
481
482
483 me.lookup('slots').getStore().setData(data.slot);
484 me.lookup('import_export').getStore().setData(data['import-export']);
485 me.lookup('drives').getStore().setData(data.drive);
486
487 Proxmox.Utils.setErrorMask(view);
488 } catch (err) {
489 Proxmox.Utils.setErrorMask(view);
490 Proxmox.Utils.setErrorMask(me.lookup('content'), err);
491 }
492 },
493
494 renderIsLabeled: function(value, mD, record) {
495 if (!record.data['label-text']) {
496 return "";
497 }
498
499 if (record.data['label-text'].startsWith("CLN")) {
500 return "";
501 }
502
503 if (!value) {
504 return gettext('Not Labeled');
505 }
506
507 let status = record.data.status;
508 if (record.data.pool) {
509 return `${status} (${record.data.pool})`;
510 }
511 return status;
512 },
513
514 renderState: function(value, md, record) {
515 if (!value) {
516 return gettext('Idle');
517 }
518
519 let icon = '<i class="fa fa-spinner fa-pulse fa-fw"></i>';
520
521 if (value.startsWith("UPID")) {
522 let upid = Proxmox.Utils.parse_task_upid(value);
523 md.tdCls = "pointer";
524 return `${icon} ${upid.desc}`;
525 }
526
527 return `${icon} ${value}`;
528 },
529
530 control: {
531 'grid[reference=drives]': {
532 cellclick: function(table, td, ci, rec, tr, ri, e) {
533 if (e.position.column.dataIndex !== 'state') {
534 return;
535 }
536
537 let upid = rec.data.state;
538 if (!upid || !upid.startsWith("UPID")) {
539 return;
540 }
541
542 Ext.create('Proxmox.window.TaskViewer', {
543 autoShow: true,
544 upid,
545 });
546 },
547 },
548 },
549 },
550
551 listeners: {
552 activate: 'reload',
553 },
554
555 tbar: [
556 {
557 fieldLabel: gettext('Changer'),
558 xtype: 'pbsChangerSelector',
559 reference: 'changerselector',
560 autoSelect: true,
561 listeners: {
562 change: 'changerChange',
563 },
564 },
565 '-',
566 {
567 text: gettext('Reload'),
568 xtype: 'proxmoxButton',
569 handler: 'reload_no_cache',
570 selModel: false,
571 },
572 '-',
573 {
574 text: gettext('Barcode Label'),
575 xtype: 'proxmoxButton',
576 handler: 'barcodeLabel',
577 iconCls: 'fa fa-barcode',
578 bind: {
579 disabled: '{!changerSelected}',
580 },
581 },
582 {
583 text: gettext('Inventory'),
584 xtype: 'proxmoxButton',
585 handler: 'inventory',
586 iconCls: 'fa fa-book',
587 bind: {
588 disabled: '{!changerSelected}',
589 },
590 },
591 ],
592
593 layout: 'auto',
594 bodyPadding: 5,
595 scrollable: true,
596
597 items: [
598 {
599 xtype: 'container',
600 reference: 'content',
601 layout: {
602 type: 'hbox',
603 aling: 'stretch',
604 },
605 items: [
606 {
607 xtype: 'grid',
608 reference: 'slots',
609 title: gettext('Slots'),
610 padding: 5,
611 flex: 1,
612 store: {
613 data: [],
614 },
615 columns: [
616 {
617 text: gettext('ID'),
618 dataIndex: 'entry-id',
619 width: 50,
620 },
621 {
622 text: gettext("Content"),
623 dataIndex: 'label-text',
624 flex: 1,
625 renderer: (value) => value || '',
626 },
627 {
628 text: gettext('Inventory'),
629 dataIndex: 'is-labeled',
630 renderer: 'renderIsLabeled',
631 flex: 1,
632 },
633 {
634 text: gettext('Actions'),
635 xtype: 'actioncolumn',
636 width: 100,
637 items: [
638 {
639 iconCls: 'fa fa-rotate-90 fa-exchange',
640 handler: 'slotTransfer',
641 tooltip: gettext('Transfer'),
642 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
643 },
644 {
645 iconCls: 'fa fa-trash-o',
646 handler: 'erase',
647 tooltip: gettext('Erase'),
648 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
649 },
650 {
651 iconCls: 'fa fa-rotate-90 fa-upload',
652 handler: 'load',
653 tooltip: gettext('Load'),
654 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
655 },
656 ],
657 },
658 ],
659 },
660 {
661 xtype: 'container',
662 flex: 2,
663 defaults: {
664 padding: 5,
665 },
666 items: [
667 {
668 xtype: 'grid',
669 reference: 'drives',
670 title: gettext('Drives'),
671 store: {
672 fields: ['entry-id', 'label-text', 'model', 'name', 'vendor', 'serial'],
673 data: [],
674 },
675 columns: [
676 {
677 text: gettext('ID'),
678 dataIndex: 'entry-id',
679 hidden: true,
680 width: 50,
681 },
682 {
683 text: gettext("Content"),
684 dataIndex: 'label-text',
685 flex: 1,
686 renderer: (value) => value || '',
687 },
688 {
689 text: gettext('Inventory'),
690 dataIndex: 'is-labeled',
691 renderer: 'renderIsLabeled',
692 flex: 1.5,
693 },
694 {
695 text: gettext("Name"),
696 sortable: true,
697 dataIndex: 'name',
698 flex: 1,
699 renderer: Ext.htmlEncode,
700 },
701 {
702 text: gettext('State'),
703 dataIndex: 'state',
704 flex: 3,
705 renderer: 'renderState',
706 },
707 {
708 text: gettext("Vendor"),
709 sortable: true,
710 dataIndex: 'vendor',
711 hidden: true,
712 flex: 1,
713 renderer: Ext.htmlEncode,
714 },
715 {
716 text: gettext("Model"),
717 sortable: true,
718 dataIndex: 'model',
719 hidden: true,
720 flex: 1,
721 renderer: Ext.htmlEncode,
722 },
723 {
724 text: gettext("Serial"),
725 sortable: true,
726 dataIndex: 'serial',
727 hidden: true,
728 flex: 1,
729 renderer: Ext.htmlEncode,
730 },
731 {
732 xtype: 'actioncolumn',
733 text: gettext('Actions'),
734 width: 140,
735 items: [
736 {
737 iconCls: 'fa fa-rotate-270 fa-upload',
738 handler: 'unload',
739 tooltip: gettext('Unload'),
740 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
741 },
742 {
743 iconCls: 'fa fa-hdd-o',
744 handler: 'cartridgeMemory',
745 tooltip: gettext('Cartridge Memory'),
746 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
747 },
748 {
749 iconCls: 'fa fa-line-chart',
750 handler: 'volumeStatistics',
751 tooltip: gettext('Volume Statistics'),
752 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
753 },
754 {
755 iconCls: 'fa fa-tag',
756 handler: 'readLabel',
757 tooltip: gettext('Read Label'),
758 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
759 },
760 {
761 iconCls: 'fa fa-info-circle',
762 tooltip: gettext('Status'),
763 handler: 'status',
764 },
765 {
766 iconCls: 'fa fa-shower',
767 tooltip: gettext('Clean Drive'),
768 handler: 'cleanDrive',
769 },
770 ],
771 },
772 ],
773 },
774 {
775 xtype: 'grid',
776 reference: 'import_export',
777 store: {
778 data: [],
779 },
780 title: gettext('Import-Export Slots'),
781 columns: [
782 {
783 text: gettext('ID'),
784 dataIndex: 'entry-id',
785 width: 50,
786 },
787 {
788 text: gettext("Content"),
789 dataIndex: 'label-text',
790 renderer: (value) => value || '',
791 flex: 1,
792 },
793 {
794 text: gettext('Inventory'),
795 dataIndex: 'is-labeled',
796 renderer: 'renderIsLabeled',
797 flex: 1,
798 },
799 {
800 text: gettext('Actions'),
801 xtype: 'actioncolumn',
802 items: [
803 {
804 iconCls: 'fa fa-rotate-270 fa-upload',
805 handler: 'importTape',
806 tooltip: gettext('Import'),
807 isDisabled: (v, r, c, i, rec) => !rec.data['label-text'],
808 },
809 ],
810 width: 80,
811 },
812 ],
813 },
814 ],
815 },
816 ],
817 },
818 ],
819 });