]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/ceph/Status.js
430d8df5a549b477872d44e22ab594651fb81334
[pve-manager.git] / www / manager6 / ceph / Status.js
1 Ext.define('pve-ceph-warnings', {
2 extend: 'Ext.data.Model',
3 fields: ['id', 'summary', 'detail', 'severity'],
4 idProperty: 'id',
5 });
6
7
8 Ext.define('PVE.node.CephStatus', {
9 extend: 'Ext.panel.Panel',
10 alias: 'widget.pveNodeCephStatus',
11
12 onlineHelp: 'chapter_pveceph',
13
14 scrollable: true,
15 bodyPadding: 5,
16 layout: {
17 type: 'column',
18 },
19
20 defaults: {
21 padding: 5,
22 },
23
24 items: [
25 {
26 xtype: 'panel',
27 title: gettext('Health'),
28 bodyPadding: 10,
29 plugins: 'responsive',
30 responsiveConfig: {
31 'width < 1600': {
32 minHeight: 230,
33 columnWidth: 1,
34 },
35 'width >= 1600': {
36 minHeight: 500,
37 columnWidth: 0.5,
38 },
39 },
40 layout: {
41 type: 'hbox',
42 align: 'stretch',
43 },
44 items: [
45 {
46 xtype: 'container',
47 layout: {
48 type: 'vbox',
49 align: 'stretch',
50 },
51 flex: 1,
52 items: [
53 {
54
55 xtype: 'pveHealthWidget',
56 itemId: 'overallhealth',
57 flex: 1,
58 title: gettext('Status'),
59 },
60 {
61 xtype: 'displayfield',
62 itemId: 'versioninfo',
63 fieldLabel: gettext('Ceph Version'),
64 value: "",
65 autoEl: {
66 tag: 'div',
67 'data-qtip': gettext('The newest version installed in the Cluster.'),
68 },
69 padding: '10 0 0 0',
70 style: {
71 'text-align': 'center',
72 },
73 },
74 ],
75 },
76 {
77 xtype: 'grid',
78 itemId: 'warnings',
79 flex: 2,
80 maxHeight: 430,
81 stateful: true,
82 stateId: 'ceph-status-warnings',
83 viewConfig: {
84 enableTextSelection: true,
85 },
86 // we load the store manually, to show an emptyText specify an empty intermediate store
87 store: {
88 type: 'diff',
89 trackRemoved: false,
90 data: [],
91 rstore: {
92 storeid: 'pve-ceph-warnings',
93 type: 'update',
94 model: 'pve-ceph-warnings',
95 },
96 },
97 updateHealth: function(health) {
98 let checks = health.checks || {};
99
100 let checkRecords = Object.keys(checks).sort().map(key => {
101 let check = checks[key];
102 let data = {
103 id: key,
104 summary: check.summary.message,
105 detail: check.detail.reduce((acc, v) => `${acc}\n${v.message}`, '').trimStart(),
106 severity: check.severity,
107 };
108 data.noDetails = data.detail.length === 0;
109 data.detailsCls = data.detail.length === 0 ? 'pmx-faded' : '';
110 if (data.detail.length === 0) {
111 data.detail = "no additional data";
112 }
113 return data;
114 });
115
116 let rstore = this.getStore().rstore;
117 rstore.loadData(checkRecords, false);
118 rstore.fireEvent('load', rstore, checkRecords, true);
119 },
120 emptyText: gettext('No Warnings/Errors'),
121 columns: [
122 {
123 dataIndex: 'severity',
124 tooltip: gettext('Severity'),
125 align: 'center',
126 width: 38,
127 renderer: function(value) {
128 let health = PVE.Utils.map_ceph_health[value];
129 let icon = PVE.Utils.get_health_icon(health);
130 return `<i class="fa fa-fw ${icon}"></i>`;
131 },
132 sorter: {
133 sorterFn: function(a, b) {
134 let health = ['HEALTH_ERR', 'HEALTH_WARN', 'HEALTH_OK'];
135 return health.indexOf(b.data.severity) - health.indexOf(a.data.severity);
136 },
137 },
138 },
139 {
140 dataIndex: 'summary',
141 header: gettext('Summary'),
142 flex: 1,
143 },
144 {
145 xtype: 'actioncolumn',
146 width: 50,
147 align: 'center',
148 tooltip: gettext('Actions'),
149 items: [
150 {
151 iconCls: 'x-fa fa-clipboard',
152 tooltip: gettext('Copy to Clipboard'),
153 handler: function(grid, rowindex, colindex, item, e, { data }) {
154 let detail = data.noDetails ? '': `\n${data.detail}`;
155 navigator.clipboard
156 .writeText(`${data.severity}: ${data.summary}${detail}`)
157 .catch(err => Ext.Msg.alert(gettext('Error'), err));
158 },
159 },
160 ],
161 },
162 ],
163 listeners: {
164 itemdblclick: function(view, record, row, rowIdx, e) {
165 // inspired by Ext.grid.plugin.RowExpander, but for double click
166 let rowNode = view.getNode(rowIdx);
167 let normalRow = Ext.fly(rowNode);
168
169 let collapsedCls = view.rowBodyFeature.rowCollapsedCls;
170
171 if (normalRow.hasCls(collapsedCls)) {
172 view.rowBodyFeature.rowExpander.toggleRow(rowIdx, record);
173 }
174 },
175 },
176 plugins: [
177 {
178 ptype: 'rowexpander',
179 expandOnDblClick: false,
180 scrollIntoViewOnExpand: false,
181 rowBodyTpl: '<pre class="pve-ceph-warning-detail {detailsCls}">{detail}</pre>',
182 },
183 ],
184 },
185 ],
186 },
187 {
188 xtype: 'pveCephStatusDetail',
189 itemId: 'statusdetail',
190 plugins: 'responsive',
191 responsiveConfig: {
192 'width < 1600': {
193 columnWidth: 1,
194 minHeight: 250,
195 },
196 'width >= 1600': {
197 columnWidth: 0.5,
198 minHeight: 300,
199 },
200 },
201 title: gettext('Status'),
202 },
203 {
204 xtype: 'pveCephServices',
205 title: gettext('Services'),
206 itemId: 'services',
207 plugins: 'responsive',
208 layout: {
209 type: 'hbox',
210 align: 'stretch',
211 },
212 responsiveConfig: {
213 'width < 1600': {
214 columnWidth: 1,
215 minHeight: 200,
216 },
217 'width >= 1600': {
218 columnWidth: 0.5,
219 minHeight: 200,
220 },
221 },
222 },
223 {
224 xtype: 'panel',
225 title: gettext('Performance'),
226 columnWidth: 1,
227 bodyPadding: 5,
228 layout: {
229 type: 'hbox',
230 align: 'center',
231 },
232 items: [
233 {
234 xtype: 'container',
235 flex: 1,
236 items: [
237 {
238 xtype: 'proxmoxGauge',
239 itemId: 'space',
240 title: gettext('Usage'),
241 },
242 {
243 flex: 1,
244 border: false,
245 },
246 {
247 xtype: 'container',
248 itemId: 'recovery',
249 hidden: true,
250 padding: 25,
251 items: [
252 {
253 xtype: 'pveRunningChart',
254 itemId: 'recoverychart',
255 title: gettext('Recovery') +'/ '+ gettext('Rebalance'),
256 renderer: PVE.Utils.render_bandwidth,
257 height: 100,
258 },
259 {
260 xtype: 'progressbar',
261 itemId: 'recoveryprogress',
262 },
263 ],
264 },
265 ],
266 },
267 {
268 xtype: 'container',
269 flex: 2,
270 defaults: {
271 padding: 0,
272 height: 100,
273 },
274 items: [
275 {
276 xtype: 'pveRunningChart',
277 itemId: 'reads',
278 title: gettext('Reads'),
279 renderer: PVE.Utils.render_bandwidth,
280 },
281 {
282 xtype: 'pveRunningChart',
283 itemId: 'writes',
284 title: gettext('Writes'),
285 renderer: PVE.Utils.render_bandwidth,
286 },
287 {
288 xtype: 'pveRunningChart',
289 itemId: 'readiops',
290 title: 'IOPS: ' + gettext('Reads'),
291 renderer: Ext.util.Format.numberRenderer('0,000'),
292 },
293 {
294 xtype: 'pveRunningChart',
295 itemId: 'writeiops',
296 title: 'IOPS: ' + gettext('Writes'),
297 renderer: Ext.util.Format.numberRenderer('0,000'),
298 },
299 ],
300 },
301 ],
302 },
303 ],
304
305 updateAll: function(store, records, success) {
306 if (!success || records.length === 0) {
307 return;
308 }
309
310 var me = this;
311 var rec = records[0];
312 me.status = rec.data;
313
314 // add health panel
315 me.down('#overallhealth').updateHealth(PVE.Utils.render_ceph_health(rec.data.health || {}));
316 me.down('#warnings').updateHealth(rec.data.health || {}); // add errors to gridstore
317
318 me.getComponent('services').updateAll(me.metadata || {}, rec.data);
319
320 me.getComponent('statusdetail').updateAll(me.metadata || {}, rec.data);
321
322 // add performance data
323 let pgmap = rec.data.pgmap;
324 let used = pgmap.bytes_used;
325 let total = pgmap.bytes_total;
326
327 var text = Ext.String.format(gettext('{0} of {1}'),
328 Proxmox.Utils.render_size(used),
329 Proxmox.Utils.render_size(total),
330 );
331
332 // update the usage widget
333 me.down('#space').updateValue(used/total, text);
334
335 let readiops = pgmap.read_op_per_sec;
336 let writeiops = pgmap.write_op_per_sec;
337 let reads = pgmap.read_bytes_sec || 0;
338 let writes = pgmap.write_bytes_sec || 0;
339
340 // update the graphs
341 me.reads.addDataPoint(reads);
342 me.writes.addDataPoint(writes);
343 me.readiops.addDataPoint(readiops);
344 me.writeiops.addDataPoint(writeiops);
345
346 let degraded = pgmap.degraded_objects || 0;
347 let misplaced = pgmap.misplaced_objects || 0;
348 let unfound = pgmap.unfound_objects || 0;
349 let unhealthy = degraded + unfound + misplaced;
350 // update recovery
351 if (pgmap.recovering_objects_per_sec !== undefined || unhealthy > 0) {
352 let toRecoverObjects = pgmap.misplaced_total || pgmap.unfound_total || pgmap.degraded_total || 0;
353 if (toRecoverObjects === 0) {
354 return; // FIXME: unexpected return and leaves things possible visible when it shouldn't?
355 }
356 let recovered = toRecoverObjects - unhealthy || 0;
357 let speed = pgmap.recovering_bytes_per_sec || 0;
358
359 let recoveryRatio = recovered / toRecoverObjects;
360 let txt = `${(recoveryRatio * 100).toFixed(2)}%`;
361 if (speed > 0) {
362 let obj_per_sec = speed / (4 * 1024 * 1024); // 4 MiB per Object
363 let duration = Proxmox.Utils.format_duration_human(unhealthy/obj_per_sec);
364 let speedTxt = PVE.Utils.render_bandwidth(speed);
365 txt += ` (${speedTxt} - ${duration} left)`;
366 }
367
368 me.down('#recovery').setVisible(true);
369 me.down('#recoveryprogress').updateValue(recoveryRatio);
370 me.down('#recoveryprogress').updateText(txt);
371 me.down('#recoverychart').addDataPoint(speed);
372 } else {
373 me.down('#recovery').setVisible(false);
374 me.down('#recoverychart').addDataPoint(0);
375 }
376 },
377
378 initComponent: function() {
379 var me = this;
380
381 var nodename = me.pveSelNode.data.node;
382
383 me.callParent();
384 var baseurl = '/api2/json' + (nodename ? '/nodes/' + nodename : '/cluster') + '/ceph';
385 me.store = Ext.create('Proxmox.data.UpdateStore', {
386 storeid: 'ceph-status-' + (nodename || 'cluster'),
387 interval: 5000,
388 proxy: {
389 type: 'proxmox',
390 url: baseurl + '/status',
391 },
392 });
393
394 me.metadatastore = Ext.create('Proxmox.data.UpdateStore', {
395 storeid: 'ceph-metadata-' + (nodename || 'cluster'),
396 interval: 15*1000,
397 proxy: {
398 type: 'proxmox',
399 url: '/api2/json/cluster/ceph/metadata',
400 },
401 });
402
403 // save references for the updatefunction
404 me.iops = me.down('#iops');
405 me.readiops = me.down('#readiops');
406 me.writeiops = me.down('#writeiops');
407 me.reads = me.down('#reads');
408 me.writes = me.down('#writes');
409
410 // manages the "install ceph?" overlay
411 PVE.Utils.monitor_ceph_installed(me, me.store, nodename);
412
413 me.mon(me.store, 'load', me.updateAll, me);
414 me.mon(me.metadatastore, 'load', function(store, records, success) {
415 if (!success || records.length < 1) {
416 return;
417 }
418 me.metadata = records[0].data;
419
420 // update services
421 me.getComponent('services').updateAll(me.metadata, me.status || {});
422
423 // update detailstatus panel
424 me.getComponent('statusdetail').updateAll(me.metadata, me.status || {});
425
426 let maxversion = [];
427 let maxversiontext = "";
428 for (const [_nodename, data] of Object.entries(me.metadata.node)) {
429 let version = data.version.parts;
430 if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
431 maxversion = version;
432 maxversiontext = data.version.str;
433 }
434 }
435 me.down('#versioninfo').setValue(maxversiontext);
436 }, me);
437
438 me.on('destroy', me.store.stopUpdate);
439 me.on('destroy', me.metadatastore.stopUpdate);
440 me.store.startUpdate();
441 me.metadatastore.startUpdate();
442 },
443
444 });