]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/ceph/OSD.js
ui: osd: add details window
[pve-manager.git] / www / manager6 / ceph / OSD.js
1 Ext.define('PVE.CephCreateOsd', {
2 extend: 'Proxmox.window.Edit',
3 xtype: 'pveCephCreateOsd',
4
5 subject: 'Ceph OSD',
6
7 showProgress: true,
8
9 onlineHelp: 'pve_ceph_osds',
10
11 initComponent: function() {
12 let me = this;
13
14 if (!me.nodename) {
15 throw "no node name specified";
16 }
17
18 me.isCreate = true;
19
20 Proxmox.Utils.API2Request({
21 url: `/nodes/${me.nodename}/ceph/crush`,
22 method: 'GET',
23 failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
24 success: function({ result: { data } }) {
25 let classes = [...new Set(
26 Array.from(
27 data.matchAll(/^device\s[0-9]*\sosd\.[0-9]*\sclass\s(.*)$/gim),
28 m => m[1],
29 ).filter(v => !['hdd', 'ssd', 'nvme'].includes(v)),
30 )].map(v => [v, v]);
31
32 if (classes.length) {
33 let kvField = me.down('field[name=crush-device-class]');
34 kvField.setComboItems([...kvField.comboItems, ...classes]);
35 }
36 },
37 });
38
39 Ext.applyIf(me, {
40 url: "/nodes/" + me.nodename + "/ceph/osd",
41 method: 'POST',
42 items: [
43 {
44 xtype: 'inputpanel',
45 onGetValues: function(values) {
46 Object.keys(values || {}).forEach(function(name) {
47 if (values[name] === '') {
48 delete values[name];
49 }
50 });
51
52 return values;
53 },
54 column1: [
55 {
56 xtype: 'pmxDiskSelector',
57 name: 'dev',
58 nodename: me.nodename,
59 diskType: 'unused',
60 includePartitions: true,
61 fieldLabel: gettext('Disk'),
62 allowBlank: false,
63 },
64 ],
65 column2: [
66 {
67 xtype: 'pmxDiskSelector',
68 name: 'db_dev',
69 nodename: me.nodename,
70 diskType: 'journal_disks',
71 includePartitions: true,
72 fieldLabel: gettext('DB Disk'),
73 value: '',
74 autoSelect: false,
75 allowBlank: true,
76 emptyText: 'use OSD disk',
77 listeners: {
78 change: function(field, val) {
79 me.down('field[name=db_dev_size]').setDisabled(!val);
80 },
81 },
82 },
83 {
84 xtype: 'numberfield',
85 name: 'db_dev_size',
86 fieldLabel: gettext('DB size') + ' (GiB)',
87 minValue: 1,
88 maxValue: 128*1024,
89 decimalPrecision: 2,
90 allowBlank: true,
91 disabled: true,
92 emptyText: gettext('Automatic'),
93 },
94 ],
95 advancedColumn1: [
96 {
97 xtype: 'proxmoxcheckbox',
98 name: 'encrypted',
99 fieldLabel: gettext('Encrypt OSD'),
100 },
101 {
102 xtype: 'proxmoxKVComboBox',
103 comboItems: [
104 ['hdd', 'HDD'],
105 ['ssd', 'SSD'],
106 ['nvme', 'NVMe'],
107 ],
108 name: 'crush-device-class',
109 nodename: me.nodename,
110 fieldLabel: gettext('Device Class'),
111 value: '',
112 autoSelect: false,
113 allowBlank: true,
114 editable: true,
115 emptyText: 'auto detect',
116 deleteEmpty: !me.isCreate,
117 },
118 ],
119 advancedColumn2: [
120 {
121 xtype: 'pmxDiskSelector',
122 name: 'wal_dev',
123 nodename: me.nodename,
124 diskType: 'journal_disks',
125 includePartitions: true,
126 fieldLabel: gettext('WAL Disk'),
127 value: '',
128 autoSelect: false,
129 allowBlank: true,
130 emptyText: 'use OSD/DB disk',
131 listeners: {
132 change: function(field, val) {
133 me.down('field[name=wal_dev_size]').setDisabled(!val);
134 },
135 },
136 },
137 {
138 xtype: 'numberfield',
139 name: 'wal_dev_size',
140 fieldLabel: gettext('WAL size') + ' (GiB)',
141 minValue: 0.5,
142 maxValue: 128*1024,
143 decimalPrecision: 2,
144 allowBlank: true,
145 disabled: true,
146 emptyText: gettext('Automatic'),
147 },
148 ],
149 },
150 {
151 xtype: 'displayfield',
152 padding: '5 0 0 0',
153 userCls: 'pmx-hint',
154 value: 'Note: Ceph is not compatible with disks backed by a hardware ' +
155 'RAID controller. For details see ' +
156 '<a target="_blank" href="' + Proxmox.Utils.get_help_link('chapter_pveceph') + '">the reference documentation</a>.',
157 },
158 ],
159 });
160
161 me.callParent();
162 },
163 });
164
165 Ext.define('PVE.CephRemoveOsd', {
166 extend: 'Proxmox.window.Edit',
167 alias: ['widget.pveCephRemoveOsd'],
168
169 isRemove: true,
170
171 showProgress: true,
172 method: 'DELETE',
173 items: [
174 {
175 xtype: 'proxmoxcheckbox',
176 name: 'cleanup',
177 checked: true,
178 labelWidth: 130,
179 fieldLabel: gettext('Cleanup Disks'),
180 },
181 {
182 xtype: 'displayfield',
183 name: 'osd-flag-hint',
184 userCls: 'pmx-hint',
185 value: gettext('Global flags limiting the self healing of Ceph are enabled.'),
186 hidden: true,
187 },
188 {
189 xtype: 'displayfield',
190 name: 'degraded-objects-hint',
191 userCls: 'pmx-hint',
192 value: gettext('Objects are degraded. Consider waiting until the cluster is healthy.'),
193 hidden: true,
194 },
195 ],
196 initComponent: function() {
197 let me = this;
198
199 if (!me.nodename) {
200 throw "no node name specified";
201 }
202 if (me.osdid === undefined || me.osdid < 0) {
203 throw "no osdid specified";
204 }
205
206 me.isCreate = true;
207
208 me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString();
209
210 Ext.applyIf(me, {
211 url: "/nodes/" + me.nodename + "/ceph/osd/" + me.osdid.toString(),
212 });
213
214 me.callParent();
215
216 if (me.warnings.flags) {
217 me.down('field[name=osd-flag-hint]').setHidden(false);
218 }
219 if (me.warnings.degraded) {
220 me.down('field[name=degraded-objects-hint]').setHidden(false);
221 }
222 },
223 });
224
225 Ext.define('PVE.CephSetFlags', {
226 extend: 'Proxmox.window.Edit',
227 xtype: 'pveCephSetFlags',
228
229 showProgress: true,
230
231 width: 720,
232 layout: 'fit',
233
234 onlineHelp: 'pve_ceph_osds',
235 isCreate: true,
236 title: Ext.String.format(gettext('Manage {0}'), 'Global OSD Flags'),
237 submitText: gettext('Apply'),
238
239 items: [
240 {
241 xtype: 'inputpanel',
242 onGetValues: function(values) {
243 let me = this;
244 let val = {};
245 me.down('#flaggrid').getStore().each((rec) => {
246 val[rec.data.name] = rec.data.value ? 1 : 0;
247 });
248
249 return val;
250 },
251 items: [
252 {
253 xtype: 'grid',
254 itemId: 'flaggrid',
255 store: {
256 listeners: {
257 update: function() {
258 this.commitChanges();
259 },
260 },
261 },
262
263 columns: [
264 {
265 text: gettext('Enable'),
266 xtype: 'checkcolumn',
267 width: 75,
268 dataIndex: 'value',
269 },
270 {
271 text: 'Name',
272 dataIndex: 'name',
273 },
274 {
275 text: 'Description',
276 flex: 1,
277 dataIndex: 'description',
278 },
279 ],
280 },
281 ],
282 },
283 ],
284
285 initComponent: function() {
286 let me = this;
287
288 if (!me.nodename) {
289 throw "no node name specified";
290 }
291
292 Ext.applyIf(me, {
293 url: "/cluster/ceph/flags",
294 method: 'PUT',
295 });
296
297 me.callParent();
298
299 let grid = me.down('#flaggrid');
300 me.load({
301 success: function(response, options) {
302 let data = response.result.data;
303 grid.getStore().setData(data);
304 // re-align after store load, else the window is not centered
305 me.alignTo(Ext.getBody(), 'c-c');
306 },
307 });
308 },
309 });
310
311 Ext.define('PVE.node.CephOsdTree', {
312 extend: 'Ext.tree.Panel',
313 alias: ['widget.pveNodeCephOsdTree'],
314 onlineHelp: 'chapter_pveceph',
315
316 viewModel: {
317 data: {
318 nodename: '',
319 flags: [],
320 maxversion: '0',
321 mixedversions: false,
322 versions: {},
323 isOsd: false,
324 downOsd: false,
325 upOsd: false,
326 inOsd: false,
327 outOsd: false,
328 osdid: '',
329 osdhost: '',
330 },
331 },
332
333 controller: {
334 xclass: 'Ext.app.ViewController',
335
336 reload: function() {
337 let me = this;
338 let view = me.getView();
339 let vm = me.getViewModel();
340 let nodename = vm.get('nodename');
341 let sm = view.getSelectionModel();
342 Proxmox.Utils.API2Request({
343 url: "/nodes/" + nodename + "/ceph/osd",
344 waitMsgTarget: view,
345 method: 'GET',
346 failure: function(response, opts) {
347 let msg = response.htmlStatus;
348 PVE.Utils.showCephInstallOrMask(view, msg, nodename, win =>
349 view.mon(win, 'cephInstallWindowClosed', me.reload),
350 );
351 },
352 success: function(response, opts) {
353 let data = response.result.data;
354 let selected = view.getSelection();
355 let name;
356 if (selected.length) {
357 name = selected[0].data.name;
358 }
359 data.versions = data.versions || {};
360 vm.set('versions', data.versions);
361 // extract max version
362 let maxversion = "0";
363 let mixedversions = false;
364 let traverse;
365 traverse = function(node, fn) {
366 fn(node);
367 if (Array.isArray(node.children)) {
368 node.children.forEach(c => { traverse(c, fn); });
369 }
370 };
371 traverse(data.root, node => {
372 // compatibility for old api call
373 if (node.type === 'host' && !node.version) {
374 node.version = data.versions[node.name];
375 }
376
377 if (node.version === undefined) {
378 return;
379 }
380
381 if (PVE.Utils.compare_ceph_versions(node.version, maxversion) !== 0 && maxversion !== "0") {
382 mixedversions = true;
383 }
384
385 if (PVE.Utils.compare_ceph_versions(node.version, maxversion) > 0) {
386 maxversion = node.version;
387 }
388 });
389 vm.set('maxversion', maxversion);
390 vm.set('mixedversions', mixedversions);
391 sm.deselectAll();
392 view.setRootNode(data.root);
393 view.expandAll();
394 if (name) {
395 let node = view.getRootNode().findChild('name', name, true);
396 if (node) {
397 view.setSelection([node]);
398 }
399 }
400
401 let flags = data.flags.split(',');
402 vm.set('flags', flags);
403 },
404 });
405 },
406
407 osd_cmd: function(comp) {
408 let me = this;
409 let vm = this.getViewModel();
410 let cmd = comp.cmd;
411 let params = comp.params || {};
412 let osdid = vm.get('osdid');
413
414 let doRequest = function() {
415 let targetnode = vm.get('osdhost');
416 // cmds not node specific and need to work if the OSD node is down
417 if (['in', 'out'].includes(cmd)) {
418 targetnode = vm.get('nodename');
419 }
420 Proxmox.Utils.API2Request({
421 url: `/nodes/${targetnode}/ceph/osd/${osdid}/${cmd}`,
422 waitMsgTarget: me.getView(),
423 method: 'POST',
424 params: params,
425 success: () => { me.reload(); },
426 failure: function(response, opts) {
427 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
428 },
429 });
430 };
431
432 if (cmd === 'scrub') {
433 Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1;
434 Ext.Msg.show({
435 title: gettext('Confirm'),
436 icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
437 msg: params.deep !== 1
438 ? Ext.String.format(gettext("Scrub OSD.{0}"), osdid)
439 : Ext.String.format(gettext("Deep Scrub OSD.{0}"), osdid) +
440 "<br>Caution: This can reduce performance while it is running.",
441 buttons: Ext.Msg.YESNO,
442 callback: function(btn) {
443 if (btn !== 'yes') {
444 return;
445 }
446 doRequest();
447 },
448 });
449 } else {
450 doRequest();
451 }
452 },
453
454 create_osd: function() {
455 let me = this;
456 let vm = this.getViewModel();
457 Ext.create('PVE.CephCreateOsd', {
458 nodename: vm.get('nodename'),
459 taskDone: () => { me.reload(); },
460 }).show();
461 },
462
463 destroy_osd: async function() {
464 let me = this;
465 let vm = this.getViewModel();
466
467 let warnings = {
468 flags: false,
469 degraded: false,
470 };
471
472 let flagsPromise = Proxmox.Async.api2({
473 url: `/cluster/ceph/flags`,
474 method: 'GET',
475 });
476
477 let statusPromise = Proxmox.Async.api2({
478 url: `/cluster/ceph/status`,
479 method: 'GET',
480 });
481
482 me.getView().mask(gettext('Loading...'));
483
484 try {
485 let result = await Promise.all([flagsPromise, statusPromise]);
486
487 let flagsData = result[0].result.data;
488 let statusData = result[1].result.data;
489
490 let flags = Array.from(
491 flagsData.filter(v => v.value),
492 v => v.name,
493 ).filter(v => ['norebalance', 'norecover', 'noout'].includes(v));
494
495 if (flags.length) {
496 warnings.flags = true;
497 }
498 if (Object.keys(statusData.pgmap).includes('degraded_objects')) {
499 warnings.degraded = true;
500 }
501 } catch (error) {
502 Ext.Msg.alert(gettext('Error'), error.htmlStatus);
503 me.getView().unmask();
504 return;
505 }
506
507 me.getView().unmask();
508 Ext.create('PVE.CephRemoveOsd', {
509 nodename: vm.get('osdhost'),
510 osdid: vm.get('osdid'),
511 warnings: warnings,
512 taskDone: () => { me.reload(); },
513 autoShow: true,
514 });
515 },
516
517 set_flags: function() {
518 let me = this;
519 let vm = this.getViewModel();
520 Ext.create('PVE.CephSetFlags', {
521 nodename: vm.get('nodename'),
522 taskDone: () => { me.reload(); },
523 }).show();
524 },
525
526 service_cmd: function(comp) {
527 let me = this;
528 let vm = this.getViewModel();
529 let cmd = comp.cmd || comp;
530
531 let doRequest = function() {
532 Proxmox.Utils.API2Request({
533 url: `/nodes/${vm.get('osdhost')}/ceph/${cmd}`,
534 params: { service: "osd." + vm.get('osdid') },
535 waitMsgTarget: me.getView(),
536 method: 'POST',
537 success: function(response, options) {
538 let upid = response.result.data;
539 let win = Ext.create('Proxmox.window.TaskProgress', {
540 upid: upid,
541 taskDone: () => { me.reload(); },
542 });
543 win.show();
544 },
545 failure: function(response, opts) {
546 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
547 },
548 });
549 };
550
551 if (cmd === "stop") {
552 Proxmox.Utils.API2Request({
553 url: `/nodes/${vm.get('osdhost')}/ceph/cmd-safety`,
554 params: {
555 service: 'osd',
556 id: vm.get('osdid'),
557 action: 'stop',
558 },
559 waitMsgTarget: me.getView(),
560 method: 'GET',
561 success: function({ result: { data } }) {
562 if (!data.safe) {
563 Ext.Msg.show({
564 title: gettext('Warning'),
565 message: data.status,
566 icon: Ext.Msg.WARNING,
567 buttons: Ext.Msg.OKCANCEL,
568 buttonText: { ok: gettext('Stop OSD') },
569 fn: function(selection) {
570 if (selection === 'ok') {
571 doRequest();
572 }
573 },
574 });
575 } else {
576 doRequest();
577 }
578 },
579 failure: response => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
580 });
581 } else {
582 doRequest();
583 }
584 },
585
586 run_details: function(view, rec) {
587 if (rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0) {
588 this.details();
589 }
590 },
591
592 details: function() {
593 let vm = this.getViewModel();
594 Ext.create('PVE.CephOsdDetails', {
595 nodename: vm.get('osdhost'),
596 osdid: vm.get('osdid'),
597 }).show();
598 },
599
600 set_selection_status: function(tp, selection) {
601 if (selection.length < 1) {
602 return;
603 }
604 let rec = selection[0];
605 let vm = this.getViewModel();
606
607 let isOsd = rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0;
608
609 vm.set('isOsd', isOsd);
610 vm.set('downOsd', isOsd && rec.data.status === 'down');
611 vm.set('upOsd', isOsd && rec.data.status !== 'down');
612 vm.set('inOsd', isOsd && rec.data.in);
613 vm.set('outOsd', isOsd && !rec.data.in);
614 vm.set('osdid', isOsd ? rec.data.id : undefined);
615 vm.set('osdhost', isOsd ? rec.data.host : undefined);
616 },
617
618 render_status: function(value, metaData, rec) {
619 if (!value) {
620 return value;
621 }
622 let inout = rec.data.in ? 'in' : 'out';
623 let updownicon = value === 'up' ? 'good fa-arrow-circle-up'
624 : 'critical fa-arrow-circle-down';
625
626 let inouticon = rec.data.in ? 'good fa-circle'
627 : 'warning fa-circle-o';
628
629 let text = value + ' <i class="fa ' + updownicon + '"></i> / ' +
630 inout + ' <i class="fa ' + inouticon + '"></i>';
631
632 return text;
633 },
634
635 render_wal: function(value, metaData, rec) {
636 if (!value &&
637 rec.data.osdtype === 'bluestore' &&
638 rec.data.type === 'osd') {
639 return 'N/A';
640 }
641 return value;
642 },
643
644 render_version: function(value, metadata, rec) {
645 let vm = this.getViewModel();
646 let versions = vm.get('versions');
647 let icon = "";
648 let version = value || "";
649 let maxversion = vm.get('maxversion');
650 if (value && PVE.Utils.compare_ceph_versions(value, maxversion) !== 0) {
651 let host_version = rec.parentNode?.data?.version || versions[rec.data.host] || "";
652 if (rec.data.type === 'host' || PVE.Utils.compare_ceph_versions(host_version, maxversion) !== 0) {
653 icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
654 } else {
655 icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
656 }
657 } else if (value && vm.get('mixedversions')) {
658 icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
659 }
660
661 return icon + version;
662 },
663
664 render_osd_val: function(value, metaData, rec) {
665 return rec.data.type === 'osd' ? value : '';
666 },
667 render_osd_weight: function(value, metaData, rec) {
668 if (rec.data.type !== 'osd') {
669 return '';
670 }
671 return Ext.util.Format.number(value, '0.00###');
672 },
673
674 render_osd_latency: function(value, metaData, rec) {
675 if (rec.data.type !== 'osd') {
676 return '';
677 }
678 let commit_ms = rec.data.commit_latency_ms,
679 apply_ms = rec.data.apply_latency_ms;
680 return apply_ms + ' / ' + commit_ms;
681 },
682
683 render_osd_size: function(value, metaData, rec) {
684 return this.render_osd_val(Proxmox.Utils.render_size(value), metaData, rec);
685 },
686
687 control: {
688 '#': {
689 selectionchange: 'set_selection_status',
690 },
691 },
692
693 init: function(view) {
694 let me = this;
695 let vm = this.getViewModel();
696
697 if (!view.pveSelNode.data.node) {
698 throw "no node name specified";
699 }
700
701 vm.set('nodename', view.pveSelNode.data.node);
702
703 me.callParent();
704 me.reload();
705 },
706 },
707
708 stateful: true,
709 stateId: 'grid-ceph-osd',
710 rootVisible: false,
711 useArrows: true,
712 listeners: {
713 itemdblclick: 'run_details',
714 },
715
716 columns: [
717 {
718 xtype: 'treecolumn',
719 text: 'Name',
720 dataIndex: 'name',
721 width: 150,
722 },
723 {
724 text: 'Type',
725 dataIndex: 'type',
726 hidden: true,
727 align: 'right',
728 width: 75,
729 },
730 {
731 text: gettext("Class"),
732 dataIndex: 'device_class',
733 align: 'right',
734 width: 75,
735 },
736 {
737 text: "OSD Type",
738 dataIndex: 'osdtype',
739 align: 'right',
740 width: 100,
741 },
742 {
743 text: "Bluestore Device",
744 dataIndex: 'blfsdev',
745 align: 'right',
746 width: 75,
747 hidden: true,
748 },
749 {
750 text: "DB Device",
751 dataIndex: 'dbdev',
752 align: 'right',
753 width: 75,
754 hidden: true,
755 },
756 {
757 text: "WAL Device",
758 dataIndex: 'waldev',
759 align: 'right',
760 renderer: 'render_wal',
761 width: 75,
762 hidden: true,
763 },
764 {
765 text: 'Status',
766 dataIndex: 'status',
767 align: 'right',
768 renderer: 'render_status',
769 width: 120,
770 },
771 {
772 text: gettext('Version'),
773 dataIndex: 'version',
774 align: 'right',
775 renderer: 'render_version',
776 },
777 {
778 text: 'weight',
779 dataIndex: 'crush_weight',
780 align: 'right',
781 renderer: 'render_osd_weight',
782 width: 90,
783 },
784 {
785 text: 'reweight',
786 dataIndex: 'reweight',
787 align: 'right',
788 renderer: 'render_osd_weight',
789 width: 90,
790 },
791 {
792 text: gettext('Used') + ' (%)',
793 dataIndex: 'percent_used',
794 align: 'right',
795 renderer: function(value, metaData, rec) {
796 if (rec.data.type !== 'osd') {
797 return '';
798 }
799 return Ext.util.Format.number(value, '0.00');
800 },
801 width: 100,
802 },
803 {
804 text: gettext('Total'),
805 dataIndex: 'total_space',
806 align: 'right',
807 renderer: 'render_osd_size',
808 width: 100,
809 },
810 {
811 text: 'Apply/Commit<br>Latency (ms)',
812 dataIndex: 'apply_latency_ms',
813 align: 'right',
814 renderer: 'render_osd_latency',
815 width: 120,
816 },
817 {
818 text: 'PGs',
819 dataIndex: 'pgs',
820 align: 'right',
821 renderer: 'render_osd_val',
822 width: 90,
823 },
824 ],
825
826
827 tbar: {
828 items: [
829 {
830 text: gettext('Reload'),
831 iconCls: 'fa fa-refresh',
832 handler: 'reload',
833 },
834 '-',
835 {
836 text: gettext('Create') + ': OSD',
837 handler: 'create_osd',
838 },
839 {
840 text: Ext.String.format(gettext('Manage {0}'), 'Global Flags'),
841 handler: 'set_flags',
842 },
843 '->',
844 {
845 xtype: 'tbtext',
846 data: {
847 osd: undefined,
848 },
849 bind: {
850 data: {
851 osd: "{osdid}",
852 },
853 },
854 tpl: [
855 '<tpl if="osd">',
856 'osd.{osd}:',
857 '<tpl else>',
858 gettext('No OSD selected'),
859 '</tpl>',
860 ],
861 },
862 {
863 text: gettext('Details'),
864 iconCls: 'fa fa-info-circle',
865 disabled: true,
866 bind: {
867 disabled: '{!isOsd}',
868 },
869 handler: 'details',
870 },
871 {
872 text: gettext('Start'),
873 iconCls: 'fa fa-play',
874 disabled: true,
875 bind: {
876 disabled: '{!downOsd}',
877 },
878 cmd: 'start',
879 handler: 'service_cmd',
880 },
881 {
882 text: gettext('Stop'),
883 iconCls: 'fa fa-stop',
884 disabled: true,
885 bind: {
886 disabled: '{!upOsd}',
887 },
888 cmd: 'stop',
889 handler: 'service_cmd',
890 },
891 {
892 text: gettext('Restart'),
893 iconCls: 'fa fa-refresh',
894 disabled: true,
895 bind: {
896 disabled: '{!upOsd}',
897 },
898 cmd: 'restart',
899 handler: 'service_cmd',
900 },
901 '-',
902 {
903 text: 'Out',
904 iconCls: 'fa fa-circle-o',
905 disabled: true,
906 bind: {
907 disabled: '{!inOsd}',
908 },
909 cmd: 'out',
910 handler: 'osd_cmd',
911 },
912 {
913 text: 'In',
914 iconCls: 'fa fa-circle',
915 disabled: true,
916 bind: {
917 disabled: '{!outOsd}',
918 },
919 cmd: 'in',
920 handler: 'osd_cmd',
921 },
922 '-',
923 {
924 text: gettext('More'),
925 iconCls: 'fa fa-bars',
926 disabled: true,
927 bind: {
928 disabled: '{!isOsd}',
929 },
930 menu: [
931 {
932 text: gettext('Scrub'),
933 iconCls: 'fa fa-shower',
934 cmd: 'scrub',
935 handler: 'osd_cmd',
936 },
937 {
938 text: gettext('Deep Scrub'),
939 iconCls: 'fa fa-bath',
940 cmd: 'scrub',
941 params: {
942 deep: 1,
943 },
944 handler: 'osd_cmd',
945 },
946 {
947 text: gettext('Destroy'),
948 itemId: 'remove',
949 iconCls: 'fa fa-fw fa-trash-o',
950 bind: {
951 disabled: '{!downOsd}',
952 },
953 handler: 'destroy_osd',
954 },
955 ],
956 },
957 ],
958 },
959
960 fields: [
961 'name', 'type', 'status', 'host', 'in', 'id',
962 { type: 'number', name: 'reweight' },
963 { type: 'number', name: 'percent_used' },
964 { type: 'integer', name: 'bytes_used' },
965 { type: 'integer', name: 'total_space' },
966 { type: 'integer', name: 'apply_latency_ms' },
967 { type: 'integer', name: 'commit_latency_ms' },
968 { type: 'string', name: 'device_class' },
969 { type: 'string', name: 'osdtype' },
970 { type: 'string', name: 'blfsdev' },
971 { type: 'string', name: 'dbdev' },
972 { type: 'string', name: 'waldev' },
973 {
974 type: 'string', name: 'version', calculate: function(data) {
975 return PVE.Utils.parse_ceph_version(data);
976 },
977 },
978 {
979 type: 'string', name: 'iconCls', calculate: function(data) {
980 let iconMap = {
981 host: 'fa-building',
982 osd: 'fa-hdd-o',
983 root: 'fa-server',
984 };
985 return `fa x-fa-tree ${iconMap[data.type] ?? 'fa-folder-o'}`;
986 },
987 },
988 { type: 'number', name: 'crush_weight' },
989 ],
990 });