]> git.proxmox.com Git - pve-manager.git/blob - www/manager6/ceph/CephInstallWizard.js
63dec054cb540f2023199e4a6c1a0cab44abab4f
[pve-manager.git] / www / manager6 / ceph / CephInstallWizard.js
1 Ext.define('PVE.ceph.CephInstallWizardInfo', {
2 extend: 'Ext.panel.Panel',
3 xtype: 'pveCephInstallWizardInfo',
4
5 html: `<h3>Ceph?</h3>
6 <blockquote cite="https://ceph.com/"><p>"<b>Ceph</b> is a unified,
7 distributed storage system, designed for excellent performance, reliability,
8 and scalability."</p></blockquote>
9 <p>
10 <b>Ceph</b> is currently <b>not installed</b> on this node. This wizard
11 will guide you through the installation. Click on the next button below
12 to begin. After the initial installation, the wizard will offer to create
13 an initial configuration. This configuration step is only
14 needed once per cluster and will be skipped if a config is already present.
15 </p>
16 <p>
17 Before starting the installation, please take a look at our documentation,
18 by clicking the help button below. If you want to gain deeper knowledge about
19 Ceph, visit <a target="_blank" href="https://docs.ceph.com/en/latest/">ceph.com</a>.
20 </p>`,
21 });
22
23 Ext.define('PVE.ceph.CephVersionSelector', {
24 extend: 'Ext.form.field.ComboBox',
25 xtype: 'pveCephVersionSelector',
26
27 fieldLabel: gettext('Ceph version to install'),
28
29 displayField: 'display',
30 valueField: 'release',
31
32 queryMode: 'local',
33 editable: false,
34 forceSelection: true,
35
36 store: {
37 fields: [
38 'release',
39 'version',
40 {
41 name: 'display',
42 calculate: d => `${d.release} (${d.version})`,
43 },
44 ],
45 proxy: {
46 type: 'memory',
47 reader: {
48 type: 'json',
49 },
50 },
51 data: [
52 { release: "quincy", version: "17.2" },
53 { release: "reef", version: "18.2" },
54 ],
55 },
56 });
57
58 Ext.define('PVE.ceph.CephHighestVersionDisplay', {
59 extend: 'Ext.form.field.Display',
60 xtype: 'pveCephHighestVersionDisplay',
61
62 fieldLabel: gettext('Ceph in the cluster'),
63
64 value: 'unknown',
65
66 // called on success with (release, versionTxt, versionParts)
67 gotNewestVersion: Ext.emptyFn,
68
69 initComponent: function() {
70 let me = this;
71
72 me.callParent(arguments);
73
74 Proxmox.Utils.API2Request({
75 method: 'GET',
76 url: '/cluster/ceph/metadata',
77 params: {
78 scope: 'versions',
79 },
80 waitMsgTarget: me,
81 success: (response) => {
82 let res = response.result;
83 if (!res || !res.data || !res.data.node) {
84 me.setValue(
85 gettext('Could not detect a ceph installation in the cluster'),
86 );
87 return;
88 }
89 let nodes = res.data.node;
90 if (me.nodename) {
91 // can happen on ceph purge, we do not yet cleanup old version data
92 delete nodes[me.nodename];
93 }
94
95 let maxversion = [];
96 let maxversiontext = "";
97 for (const [_nodename, data] of Object.entries(nodes)) {
98 let version = data.version.parts;
99 if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
100 maxversion = version;
101 maxversiontext = data.version.str;
102 }
103 }
104 // FIXME: get from version selector store
105 const major2release = {
106 13: 'luminous',
107 14: 'nautilus',
108 15: 'octopus',
109 16: 'pacific',
110 17: 'quincy',
111 18: 'reef',
112 19: 'squid',
113 };
114 let release = major2release[maxversion[0]] || 'unknown';
115 let newestVersionTxt = `${Ext.String.capitalize(release)} (${maxversiontext})`;
116
117 if (release === 'unknown') {
118 me.setValue(
119 gettext('Could not detect a ceph installation in the cluster'),
120 );
121 } else {
122 me.setValue(Ext.String.format(
123 gettext('Newest ceph version in cluster is {0}'),
124 newestVersionTxt,
125 ));
126 }
127 me.gotNewestVersion(release, maxversiontext, maxversion);
128 },
129 failure: function(response, opts) {
130 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
131 },
132 });
133 },
134 });
135
136 Ext.define('PVE.ceph.CephInstallWizard', {
137 extend: 'PVE.window.Wizard',
138 alias: 'widget.pveCephInstallWizard',
139 mixins: ['Proxmox.Mixin.CBind'],
140
141 resizable: false,
142 nodename: undefined,
143
144 width: 760, // 4:3
145 height: 570,
146
147 viewModel: {
148 data: {
149 nodename: '',
150 cephRelease: 'quincy',
151 cephRepo: 'enterprise',
152 configuration: true,
153 isInstalled: false,
154 nodeHasSubscription: true, // avoid warning hint until fully loaded
155 allHaveSubscription: true, // avoid warning hint until fully loaded
156 },
157 formulas: {
158 repoHintHidden: get => get('allHaveSubscription') && get('cephRepo') === 'enterprise',
159 repoHint: function(get) {
160 let repo = get('cephRepo');
161 let nodeSub = get('nodeHasSubscription'), allSub = get('allHaveSubscription');
162
163 if (repo === 'enterprise') {
164 if (!nodeSub) {
165 return gettext('The enterprise repository is enabled, but there is no active subscription!');
166 } else if (!allSub) {
167 return gettext('Not all nodes have an active subscription, which is required for cluster-wide enterprise repo access');
168 }
169 return ''; // should be hidden
170 } else if (repo === 'no-subscription') {
171 return allSub
172 ? gettext("Cluster has active subscriptions and would be elligible for using the enterprise repository.")
173 : gettext("The no-subscription repository is not the best choice for production setups.");
174 } else {
175 return gettext('The test repository should only be used for test setups or after consulting the official Proxmox support!');
176 }
177 },
178 },
179 },
180 cbindData: {
181 nodename: undefined,
182 },
183
184 title: gettext('Setup'),
185 navigateNext: function() {
186 var tp = this.down('#wizcontent');
187 var atab = tp.getActiveTab();
188
189 var next = tp.items.indexOf(atab) + 1;
190 var ntab = tp.items.getAt(next);
191 if (ntab) {
192 ntab.enable();
193 tp.setActiveTab(ntab);
194 }
195 },
196 setInitialTab: function(index) {
197 var tp = this.down('#wizcontent');
198 var initialTab = tp.items.getAt(index);
199 initialTab.enable();
200 tp.setActiveTab(initialTab);
201 },
202 onShow: function() {
203 this.callParent(arguments);
204 let viewModel = this.getViewModel();
205 var isInstalled = this.getViewModel().get('isInstalled');
206 if (isInstalled) {
207 viewModel.set('configuration', false);
208 this.setInitialTab(2);
209 }
210
211 PVE.Utils.getClusterSubscriptionLevel().then(subcriptionMap => {
212 viewModel.set('nodeHasSubscription', !!subcriptionMap[this.nodename]);
213
214 let allHaveSubscription = Object.values(subcriptionMap).every(level => !!level);
215 viewModel.set('allHaveSubscription', allHaveSubscription);
216 });
217 },
218 items: [
219 {
220 xtype: 'panel',
221 title: gettext('Info'),
222 viewModel: {}, // needed to inherit parent viewModel data
223 border: false,
224 bodyBorder: false,
225 onlineHelp: 'chapter_pveceph',
226 layout: {
227 type: 'vbox',
228 align: 'stretch',
229 },
230 defaults: {
231 border: false,
232 bodyBorder: false,
233 },
234 items: [
235 {
236 xtype: 'pveCephInstallWizardInfo',
237 },
238 {
239 flex: 1,
240 },
241 {
242 xtype: 'displayfield',
243 fieldLabel: gettext('Hint'),
244 labelClsExtra: 'pmx-hint',
245 submitValue: false,
246 labelWidth: 50,
247 bind: {
248 value: '{repoHint}',
249 hidden: '{repoHintHidden}',
250 },
251 },
252 {
253 xtype: 'pveCephHighestVersionDisplay',
254 labelWidth: 150,
255 cbind: {
256 nodename: '{nodename}',
257 },
258 gotNewestVersion: function(release, maxversiontext, maxversion) {
259 if (release === 'unknown') {
260 return;
261 }
262 let wizard = this.up('pveCephInstallWizard');
263 wizard.getViewModel().set('cephRelease', release);
264 },
265 },
266 {
267 xtype: 'container',
268 layout: 'hbox',
269 defaults: {
270 border: false,
271 layout: 'anchor',
272 flex: 1,
273 },
274 items: [{
275 xtype: 'pveCephVersionSelector',
276 labelWidth: 150,
277 padding: '0 10 0 0',
278 submitValue: false,
279 bind: {
280 value: '{cephRelease}',
281 },
282 listeners: {
283 change: function(field, release) {
284 let wizard = this.up('pveCephInstallWizard');
285 wizard.down('#next').setText(
286 Ext.String.format(gettext('Start {0} installation'), release),
287 );
288 },
289 },
290 },
291 {
292 xtype: 'proxmoxKVComboBox',
293 fieldLabel: gettext('Repository'),
294 padding: '0 0 0 10',
295 comboItems: [
296 ['enterprise', gettext('Enterprise (recommended)')],
297 ['no-subscription', gettext('No-Subscription')],
298 ['test', gettext('Test')],
299 ],
300 labelWidth: 150,
301 submitValue: false,
302 value: 'enterprise',
303 bind: {
304 value: '{cephRepo}',
305 },
306 }],
307 },
308 ],
309 listeners: {
310 activate: function() {
311 // notify owning container that it should display a help button
312 if (this.onlineHelp) {
313 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
314 }
315 let wizard = this.up('pveCephInstallWizard');
316 let release = wizard.getViewModel().get('cephRelease');
317 wizard.down('#back').hide(true);
318 wizard.down('#next').setText(
319 Ext.String.format(gettext('Start {0} installation'), release),
320 );
321 },
322 deactivate: function() {
323 if (this.onlineHelp) {
324 Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
325 }
326 this.up('pveCephInstallWizard').down('#next').setText(gettext('Next'));
327 },
328 },
329 },
330 {
331 title: gettext('Installation'),
332 xtype: 'panel',
333 layout: 'fit',
334 cbind: {
335 nodename: '{nodename}',
336 },
337 viewModel: {}, // needed to inherit parent viewModel data
338 listeners: {
339 afterrender: function() {
340 var me = this;
341 if (this.getViewModel().get('isInstalled')) {
342 this.mask("Ceph is already installed, click next to create your configuration.", ['pve-static-mask']);
343 } else {
344 me.down('pveNoVncConsole').fireEvent('activate');
345 }
346 },
347 activate: function() {
348 let me = this;
349 const nodename = me.nodename;
350 me.updateStore = Ext.create('Proxmox.data.UpdateStore', {
351 storeid: 'ceph-status-' + nodename,
352 interval: 1000,
353 proxy: {
354 type: 'proxmox',
355 url: '/api2/json/nodes/' + nodename + '/ceph/status',
356 },
357 listeners: {
358 load: function(rec, response, success, operation) {
359 if (success) {
360 me.updateStore.stopUpdate();
361 me.down('textfield').setValue('success');
362 } else if (operation.error.statusText.match("not initialized", "i")) {
363 me.updateStore.stopUpdate();
364 me.up('pveCephInstallWizard').getViewModel().set('configuration', false);
365 me.down('textfield').setValue('success');
366 } else if (operation.error.statusText.match("rados_connect failed", "i")) {
367 me.updateStore.stopUpdate();
368 me.up('pveCephInstallWizard').getViewModel().set('configuration', true);
369 me.down('textfield').setValue('success');
370 } else if (!operation.error.statusText.match("not installed", "i")) {
371 Proxmox.Utils.setErrorMask(me, operation.error.statusText);
372 }
373 },
374 },
375 });
376 me.updateStore.startUpdate();
377 },
378 destroy: function() {
379 var me = this;
380 if (me.updateStore) {
381 me.updateStore.stopUpdate();
382 }
383 },
384 },
385 items: [
386 {
387 xtype: 'pveNoVncConsole',
388 itemId: 'jsconsole',
389 consoleType: 'cmd',
390 xtermjs: true,
391 cbind: {
392 nodename: '{nodename}',
393 },
394 beforeLoad: function() {
395 let me = this;
396 let wizard = me.up('pveCephInstallWizard');
397 let release = wizard.getViewModel().get('cephRelease');
398 let repo = wizard.getViewModel().get('cephRepo');
399 me.cmdOpts = `--version\0${release}\0--repository\0${repo}`;
400 },
401 cmd: 'ceph_install',
402 },
403 {
404 xtype: 'textfield',
405 name: 'installSuccess',
406 value: '',
407 allowBlank: false,
408 submitValue: false,
409 hidden: true,
410 },
411 ],
412 },
413 {
414 xtype: 'inputpanel',
415 title: gettext('Configuration'),
416 onlineHelp: 'chapter_pveceph',
417 height: 300,
418 cbind: {
419 nodename: '{nodename}',
420 },
421 viewModel: {
422 data: {
423 replicas: undefined,
424 minreplicas: undefined,
425 },
426 },
427 listeners: {
428 activate: function() {
429 this.up('pveCephInstallWizard').down('#submit').setText(gettext('Next'));
430 },
431 afterrender: function() {
432 if (this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
433 this.mask("Configuration already initialized", ['pve-static-mask']);
434 } else {
435 this.unmask();
436 }
437 },
438 deactivate: function() {
439 this.up('pveCephInstallWizard').down('#submit').setText(gettext('Finish'));
440 },
441 },
442 column1: [
443 {
444 xtype: 'displayfield',
445 value: gettext('Ceph cluster configuration') + ':',
446 },
447 {
448 xtype: 'proxmoxNetworkSelector',
449 name: 'network',
450 value: '',
451 fieldLabel: 'Public Network IP/CIDR',
452 autoSelect: false,
453 bind: {
454 allowBlank: '{configuration}',
455 },
456 cbind: {
457 nodename: '{nodename}',
458 },
459 },
460 {
461 xtype: 'proxmoxNetworkSelector',
462 name: 'cluster-network',
463 fieldLabel: 'Cluster Network IP/CIDR',
464 allowBlank: true,
465 autoSelect: false,
466 emptyText: gettext('Same as Public Network'),
467 cbind: {
468 nodename: '{nodename}',
469 },
470 },
471 // FIXME: add hint about cluster network and/or reference user to docs??
472 ],
473 column2: [
474 {
475 xtype: 'displayfield',
476 value: gettext('First Ceph monitor') + ':',
477 },
478 {
479 xtype: 'displayfield',
480 fieldLabel: gettext('Monitor node'),
481 cbind: {
482 value: '{nodename}',
483 },
484 },
485 {
486 xtype: 'displayfield',
487 value: gettext('Additional monitors are recommended. They can be created at any time in the Monitor tab.'),
488 userCls: 'pmx-hint',
489 },
490 ],
491 advancedColumn1: [
492 {
493 xtype: 'numberfield',
494 name: 'size',
495 fieldLabel: 'Number of replicas',
496 bind: {
497 value: '{replicas}',
498 },
499 maxValue: 7,
500 minValue: 2,
501 emptyText: '3',
502 },
503 {
504 xtype: 'numberfield',
505 name: 'min_size',
506 fieldLabel: 'Minimum replicas',
507 bind: {
508 maxValue: '{replicas}',
509 value: '{minreplicas}',
510 },
511 minValue: 2,
512 maxValue: 3,
513 setMaxValue: function(value) {
514 this.maxValue = Ext.Number.from(value, 2);
515 // allow enough to avoid split brains with max 'size', but more makes simply no sense
516 if (this.maxValue > 4) {
517 this.maxValue = 4;
518 }
519 this.toggleSpinners();
520 this.validate();
521 },
522 emptyText: '2',
523 },
524 ],
525 onGetValues: function(values) {
526 ['cluster-network', 'size', 'min_size'].forEach(function(field) {
527 if (!values[field]) {
528 delete values[field];
529 }
530 });
531 return values;
532 },
533 onSubmit: function() {
534 var me = this;
535 if (!this.up('pveCephInstallWizard').getViewModel().get('configuration')) {
536 var wizard = me.up('window');
537 var kv = wizard.getValues();
538 delete kv.delete;
539 var nodename = me.nodename;
540 delete kv.nodename;
541 Proxmox.Utils.API2Request({
542 url: `/nodes/${nodename}/ceph/init`,
543 waitMsgTarget: wizard,
544 method: 'POST',
545 params: kv,
546 success: function() {
547 Proxmox.Utils.API2Request({
548 url: `/nodes/${nodename}/ceph/mon/${nodename}`,
549 waitMsgTarget: wizard,
550 method: 'POST',
551 success: function() {
552 me.up('pveCephInstallWizard').navigateNext();
553 },
554 failure: function(response, opts) {
555 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
556 },
557 });
558 },
559 failure: function(response, opts) {
560 Ext.Msg.alert(gettext('Error'), response.htmlStatus);
561 },
562 });
563 } else {
564 me.up('pveCephInstallWizard').navigateNext();
565 }
566 },
567 },
568 {
569 title: gettext('Success'),
570 xtype: 'panel',
571 border: false,
572 bodyBorder: false,
573 onlineHelp: 'pve_ceph_install',
574 html: '<h3>Installation successful!</h3>'+
575 '<p>The basic installation and configuration is complete. Depending on your setup, some of the following steps are required to start using Ceph:</p>'+
576 '<ol><li>Install Ceph on other nodes</li>'+
577 '<li>Create additional Ceph Monitors</li>'+
578 '<li>Create Ceph OSDs</li>'+
579 '<li>Create Ceph Pools</li></ol>'+
580 '<p>To learn more, click on the help button below.</p>',
581 listeners: {
582 activate: function() {
583 // notify owning container that it should display a help button
584 if (this.onlineHelp) {
585 Ext.GlobalEvents.fireEvent('proxmoxShowHelp', this.onlineHelp);
586 }
587
588 var tp = this.up('#wizcontent');
589 var idx = tp.items.indexOf(this)-1;
590 for (;idx >= 0; idx--) {
591 var nc = tp.items.getAt(idx);
592 if (nc) {
593 nc.disable();
594 }
595 }
596 },
597 deactivate: function() {
598 if (this.onlineHelp) {
599 Ext.GlobalEvents.fireEvent('proxmoxHideHelp', this.onlineHelp);
600 }
601 },
602 },
603 onSubmit: function() {
604 var wizard = this.up('pveCephInstallWizard');
605 wizard.close();
606 },
607 },
608 ],
609 });