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