Ext.define('PVE.CephCreateOsd', {
extend: 'Proxmox.window.Edit',
xtype: 'pveCephCreateOsd',
subject: 'Ceph OSD',
showProgress: true,
onlineHelp: 'pve_ceph_osds',
initComponent: function () {
let me = this;
if (!me.nodename) {
throw 'no node name specified';
}
me.isCreate = true;
Proxmox.Utils.API2Request({
url: `/nodes/${me.nodename}/ceph/crush`,
method: 'GET',
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
success: function ({ result: { data } }) {
let classes = [
...new Set(
Array.from(
data.matchAll(/^device\s[0-9]*\sosd\.[0-9]*\sclass\s(.*)$/gim),
(m) => m[1],
).filter((v) => !['hdd', 'ssd', 'nvme'].includes(v)),
),
].map((v) => [v, v]);
if (classes.length) {
let kvField = me.down('field[name=crush-device-class]');
kvField.setComboItems([...kvField.comboItems, ...classes]);
}
},
});
Ext.applyIf(me, {
url: '/nodes/' + me.nodename + '/ceph/osd',
method: 'POST',
items: [
{
xtype: 'inputpanel',
onGetValues: function (values) {
Object.keys(values || {}).forEach(function (name) {
if (values[name] === '') {
delete values[name];
}
});
return values;
},
column1: [
{
xtype: 'pmxDiskSelector',
name: 'dev',
nodename: me.nodename,
diskType: 'unused',
includePartitions: true,
fieldLabel: gettext('Disk'),
allowBlank: false,
},
],
column2: [
{
xtype: 'pmxDiskSelector',
name: 'db_dev',
nodename: me.nodename,
diskType: 'journal_disks',
includePartitions: true,
fieldLabel: gettext('DB Disk'),
value: '',
autoSelect: false,
allowBlank: true,
emptyText: gettext('use OSD disk'),
listeners: {
change: function (field, val) {
me.down('field[name=db_dev_size]').setDisabled(!val);
},
},
},
{
xtype: 'numberfield',
name: 'db_dev_size',
fieldLabel: `${gettext('DB size')} (${gettext('GiB')})`,
minValue: 1,
maxValue: 128 * 1024,
decimalPrecision: 2,
allowBlank: true,
disabled: true,
emptyText: gettext('Automatic'),
},
],
advancedColumn1: [
{
xtype: 'proxmoxcheckbox',
name: 'encrypted',
fieldLabel: gettext('Encrypt OSD'),
},
{
xtype: 'proxmoxKVComboBox',
comboItems: [
['hdd', 'HDD'],
['ssd', 'SSD'],
['nvme', 'NVMe'],
],
name: 'crush-device-class',
nodename: me.nodename,
fieldLabel: gettext('Device Class'),
value: '',
autoSelect: false,
allowBlank: true,
editable: true,
emptyText: gettext('auto detect'),
deleteEmpty: !me.isCreate,
},
],
advancedColumn2: [
{
xtype: 'pmxDiskSelector',
name: 'wal_dev',
nodename: me.nodename,
diskType: 'journal_disks',
includePartitions: true,
fieldLabel: gettext('WAL Disk'),
value: '',
autoSelect: false,
allowBlank: true,
emptyText: gettext('use OSD/DB disk'),
listeners: {
change: function (field, val) {
me.down('field[name=wal_dev_size]').setDisabled(!val);
},
},
},
{
xtype: 'numberfield',
name: 'wal_dev_size',
fieldLabel: `${gettext('WAL size')} (${gettext('GiB')})`,
minValue: 0.5,
maxValue: 128 * 1024,
decimalPrecision: 2,
allowBlank: true,
disabled: true,
emptyText: gettext('Automatic'),
},
],
},
{
xtype: 'displayfield',
padding: '5 0 0 0',
userCls: 'pmx-hint',
value:
'Note: Ceph is not compatible with disks backed by a hardware ' +
'RAID controller. For details see ' +
'the reference documentation.',
},
],
});
me.callParent();
},
});
Ext.define('PVE.CephRemoveOsd', {
extend: 'Proxmox.window.Edit',
alias: ['widget.pveCephRemoveOsd'],
isRemove: true,
showProgress: true,
method: 'DELETE',
items: [
{
xtype: 'proxmoxcheckbox',
name: 'cleanup',
checked: true,
labelWidth: 130,
fieldLabel: gettext('Cleanup Disks'),
},
{
xtype: 'displayfield',
name: 'osd-flag-hint',
userCls: 'pmx-hint',
value: gettext('Global flags limiting the self healing of Ceph are enabled.'),
hidden: true,
},
{
xtype: 'displayfield',
name: 'degraded-objects-hint',
userCls: 'pmx-hint',
value: gettext('Objects are degraded. Consider waiting until the cluster is healthy.'),
hidden: true,
},
],
initComponent: function () {
let me = this;
if (!me.nodename) {
throw 'no node name specified';
}
if (me.osdid === undefined || me.osdid < 0) {
throw 'no osdid specified';
}
me.isCreate = true;
me.title = gettext('Destroy') + ': Ceph OSD osd.' + me.osdid.toString();
Ext.applyIf(me, {
url: '/nodes/' + me.nodename + '/ceph/osd/' + me.osdid.toString(),
});
me.callParent();
if (me.warnings.flags) {
me.down('field[name=osd-flag-hint]').setHidden(false);
}
if (me.warnings.degraded) {
me.down('field[name=degraded-objects-hint]').setHidden(false);
}
},
});
Ext.define('PVE.CephSetFlags', {
extend: 'Proxmox.window.Edit',
xtype: 'pveCephSetFlags',
showProgress: true,
width: 720,
layout: 'fit',
onlineHelp: 'pve_ceph_osds',
isCreate: true,
title: gettext('Manage Global OSD Flags'),
submitText: gettext('Apply'),
items: [
{
xtype: 'inputpanel',
onGetValues: function (values) {
let me = this;
let val = {};
me.down('#flaggrid')
.getStore()
.each((rec) => {
val[rec.data.name] = rec.data.value ? 1 : 0;
});
return val;
},
items: [
{
xtype: 'grid',
itemId: 'flaggrid',
store: {
listeners: {
update: function () {
this.commitChanges();
},
},
},
columns: [
{
text: gettext('Enable'),
xtype: 'checkcolumn',
width: 75,
dataIndex: 'value',
},
{
text: 'Name',
dataIndex: 'name',
},
{
text: 'Description',
flex: 1,
dataIndex: 'description',
renderer: Ext.htmlEncode,
},
],
},
],
},
],
initComponent: function () {
let me = this;
if (!me.nodename) {
throw 'no node name specified';
}
Ext.applyIf(me, {
url: '/cluster/ceph/flags',
method: 'PUT',
});
me.callParent();
let grid = me.down('#flaggrid');
me.load({
success: function (response, options) {
let data = response.result.data;
grid.getStore().setData(data);
// re-align after store load, else the window is not centered
me.alignTo(Ext.getBody(), 'c-c');
},
});
},
});
Ext.define('PVE.window.CephBulkRestartOSDs', {
extend: 'Ext.window.Window',
width: 600,
title: gettext('Bulk Restart OSDs'),
modal: true,
resizable: false,
layout: 'fit',
// required: { host: count, ... } enumerated from the tree
osdsByHost: undefined,
// optional: the currently-navigated node, used to pre-select that node in the combo
nodename: undefined,
// required: function(upid) called after the API accepts the request
onTaskStarted: undefined,
initComponent: function () {
let me = this;
if (!me.osdsByHost || !me.onTaskStarted) {
throw 'osdsByHost and onTaskStarted are required';
}
let totalOsds = Object.values(me.osdsByHost).reduce((a, b) => a + b, 0);
let hosts = Object.keys(me.osdsByHost).sort();
// Combo store: only per-host entries. Cluster-wide scope is its own checkbox
// below that disables the combo when checked.
let hostStore = hosts.map((host) => {
let c = me.osdsByHost[host];
return {
value: host,
label: Ext.String.format(
ngettext("Node '{0}' ({1} OSD)", "Node '{0}' ({1} OSDs)", c),
host,
c,
),
};
});
// Default node: the currently-navigated node if it has OSDs, otherwise the
// first node with OSDs. The 'all nodes' checkbox starts off (per-node action
// is the common case and the dialog opened from a node-level toolbar).
let defaultNode = me.nodename && me.osdsByHost[me.nodename] ? me.nodename : hosts[0];
// Populated asynchronously from /cluster/ceph/metadata. Keyed by host:
// count of OSDs whose ceph_version_short differs from the host's installed
// ceph package version. Used when the 'only outdated' checkbox is on.
// Falls back to "up to N" wording while undefined or if the fetch failed.
let outdatedByHost; // { host: count } once loaded, undef while pending
let computeAffected = function (allNodes, node, onlyOutdated) {
let maxCount = allNodes ? totalOsds : me.osdsByHost[node] || 0;
let scope = allNodes
? gettext('cluster-wide')
: Ext.String.format(gettext("on node '{0}'"), node);
let count, affected;
if (onlyOutdated && outdatedByHost) {
count = allNodes
? Object.values(outdatedByHost).reduce((a, b) => a + b, 0)
: outdatedByHost[node] || 0;
affected = Ext.String.format(
ngettext(
'{0} outdated OSD {1} (of {2} total)',
'{0} outdated OSDs {1} (of {2} total)',
count,
),
count,
scope,
maxCount,
);
} else if (onlyOutdated) {
// metadata not yet loaded - acknowledge uncertainty
count = maxCount;
affected = Ext.String.format(
ngettext(
'up to {0} OSD {1} (outdated filter, exact count at task start)',
'up to {0} OSDs {1} (outdated filter, exact count at task start)',
maxCount,
),
maxCount,
scope,
);
} else {
count = maxCount;
affected = Ext.String.format(
ngettext('{0} OSD {1}', '{0} OSDs {1}', count),
count,
scope,
);
}
let mins = Math.max(2, count * 2);
return {
affected,
duration: Ext.String.format(
gettext(
'Approximately 2 minutes per OSD (total {0} minutes, depending on cluster recovery speed).',
),
mins,
),
};
};
let initial = computeAffected(false, defaultNode, false);
let refreshDisplay = function () {
let allNodes = me.down('#allNodesCheck').getValue();
let node = me.down('#nodeSelector').getValue();
let onlyOutdated = me.down('#onlyOutdatedCheck').getValue();
let info = computeAffected(allNodes, node, onlyOutdated);
me.down('#affectedField').setValue(info.affected);
me.down('#durationField').setValue(info.duration);
};
// Compute outdated counts from cluster metadata: per host, count OSDs whose
// running ceph_version_short does not match the host's installed package
// version. An approximation of the backend's exact check (which compares
// full "version (commit)" tuples); the task log shows authoritative numbers.
// Fetched lazily the first time the "only outdated" filter is enabled, so a
// plain per-node restart needs no cluster metadata round-trip (which also
// spares users holding only Sys.Modify a pointless 403 on every dialog open).
let outdatedRequested = false;
let loadOutdatedCounts = function () {
if (outdatedRequested) {
return;
}
outdatedRequested = true;
Proxmox.Utils.API2Request({
url: '/cluster/ceph/metadata',
method: 'GET',
params: { scope: 'all' },
success: function (response) {
if (me.destroyed) {
return;
}
let d = response.result.data || {};
let nodeVer = {};
for (const [n, info] of Object.entries(d.node || {})) {
nodeVer[n] = info?.version?.str;
}
let counts = {};
for (const osd of d.osd || []) {
let host = osd.hostname;
if (!host) {
continue;
}
if (!Object.hasOwn(nodeVer, host) && !Object.hasOwn(me.osdsByHost, host)) {
let short = host.split('.')[0];
if (
Object.hasOwn(nodeVer, short) ||
Object.hasOwn(me.osdsByHost, short)
) {
host = short;
}
}
counts[host] ||= 0;
let installed = nodeVer[host];
if (!installed || osd.ceph_version_short !== installed) {
counts[host]++;
}
}
outdatedByHost = counts;
refreshDisplay();
},
failure: () => {
/* leave outdatedByHost undef; UI shows "up to N" wording */
},
});
};
// Warn before submit rather than letting the task error explain it. Only the checks
// the backend calls blocking, so an upgraded cluster is not warned about a restart
// that then succeeds.
Proxmox.Utils.API2Request({
url: '/cluster/ceph/status',
method: 'GET',
success: function (response) {
if (me.destroyed) {
return;
}
let checks = response.result.data?.health?.checks || {};
let blocking = Object.keys(checks)
.sort()
.filter((k) => checks[k]['blocks-restart']?.osd);
if (!blocking.length) {
return;
}
let items = blocking
.map(
(k) =>
'
' +
Ext.String.htmlEncode(checks[k].summary?.message || k) +
'',
)
.join('');
let forceable = blocking.every((k) => checks[k].severity !== 'HEALTH_ERR');
me.down('#forceCheck').setHidden(!forceable);
let hint = me.down('#healthHint');
hint.setValue(
' ' +
(forceable
? gettext(
'These block a rolling restart. Enable Force to proceed' +
' anyway:',
)
: gettext(
'These block a rolling restart and Force cannot override' +
' them:',
)) +
``,
);
hint.setHidden(false);
},
failure: (response) => {
if (me.destroyed) {
return;
}
// A 403 only means this user may not read the health, the backend still can,
// so the restart runs as usual and force stays available. Anything else means
// the backend hits the same wall, and force cannot override that.
let hint = me.down('#healthHint');
if (response.status === 403) {
me.down('#forceCheck').setHidden(false);
return;
}
hint.setValue(
' ' +
Ext.String.format(
gettext(
'The cluster health could not be read ({0}). A restart is likely' +
' to be refused for the same reason, so check the cluster' +
' status first.',
),
Ext.String.htmlEncode(response.htmlStatus || response.status),
),
);
hint.setHidden(false);
},
});
Ext.apply(me, {
items: [
{
xtype: 'form',
bodyPadding: 10,
border: false,
fieldDefaults: { labelWidth: 100, anchor: '100%' },
items: [
{
xtype: 'combobox',
itemId: 'nodeSelector',
fieldLabel: gettext('Node'),
editable: false,
queryMode: 'local',
valueField: 'value',
displayField: 'label',
store: {
fields: ['value', 'label'],
data: hostStore,
},
value: defaultNode,
listeners: { change: refreshDisplay },
},
{
xtype: 'fieldcontainer',
fieldLabel: gettext('Apply to'),
layout: 'hbox',
items: [
{
xtype: 'proxmoxcheckbox',
itemId: 'allNodesCheck',
boxLabel: gettext('all nodes (cluster-wide)'),
uncheckedValue: 0,
checked: false,
margin: '0 20 0 0',
listeners: {
change: function (cb, val) {
me.down('#nodeSelector').setDisabled(!!val);
refreshDisplay();
},
},
},
{
xtype: 'proxmoxcheckbox',
itemId: 'onlyOutdatedCheck',
boxLabel: gettext('only outdated OSD versions'),
boxLabelAttrTpl:
'data-qtip="' +
Ext.String.htmlEncode(
gettext(
'Restart only OSDs whose running version differs' +
' from the locally-installed ceph-osd' +
' binary on each node. Useful for' +
' post-upgrade rolling restarts.',
),
) +
'"',
uncheckedValue: 0,
checked: false,
listeners: {
change: function (cb, val) {
if (val) {
loadOutdatedCounts();
}
refreshDisplay();
},
},
},
],
},
{
xtype: 'displayfield',
itemId: 'affectedField',
fieldLabel: gettext('Affected'),
value: initial.affected,
},
{
xtype: 'displayfield',
itemId: 'durationField',
fieldLabel: gettext('Duration'),
value: initial.duration,
},
{
xtype: 'displayfield',
fieldLabel: gettext('Notes'),
value: gettext(
"OSDs are restarted serially with a per-step 'ok-to-stop' check." +
" 'noout' is applied per-OSD for the duration and unset on" +
' completion. A host crash or SIGKILL of the task can leave' +
" 'noout' set on the affected OSDs; see the Ceph chapter of" +
' the Proxmox VE documentation for cleanup steps.',
),
},
// last, so that a cluster with blocking checks grows the dialog
// downwards instead of pushing every field further down
{
xtype: 'displayfield',
itemId: 'healthHint',
fieldLabel: gettext('Health Issues'),
hidden: true,
},
{
xtype: 'proxmoxcheckbox',
itemId: 'forceCheck',
fieldLabel: gettext('Force'),
boxLabel: gettext('proceed past the blocking health checks'),
hidden: true,
uncheckedValue: 0,
checked: false,
},
],
},
],
buttons: [
{
xtype: 'proxmoxHelpButton',
onlineHelp: 'pve_ceph_osds',
hidden: false,
},
'->',
{ text: gettext('Cancel'), handler: () => me.close() },
{
text: gettext('Restart'),
iconCls: 'fa fa-refresh',
handler: function () {
let allNodes = me.down('#allNodesCheck').getValue();
let onlyOutdated = me.down('#onlyOutdatedCheck').getValue();
let url = allNodes
? '/cluster/ceph/restart-bulk'
: `/nodes/${me.down('#nodeSelector').getValue()}/ceph/restart-bulk`;
let params = { 'service-type': 'osd' };
if (onlyOutdated) {
params['only-outdated'] = 1;
}
if (me.down('#forceCheck').getValue()) {
params.force = 1;
}
Proxmox.Utils.API2Request({
url,
method: 'POST',
params,
waitMsgTarget: me,
success: function (response) {
me.close();
me.onTaskStarted(response.result.data);
},
failure: (response) =>
Ext.Msg.alert(gettext('Error'), response.htmlStatus),
});
},
},
],
});
me.callParent();
},
});
Ext.define('PVE.node.CephOsdTree', {
extend: 'Ext.tree.Panel',
alias: ['widget.pveNodeCephOsdTree'],
onlineHelp: 'chapter_pveceph',
viewModel: {
data: {
nodename: '',
flags: [],
maxversion: '0',
mixedversions: false,
versions: {},
isOsd: false,
downOsd: false,
upOsd: false,
inOsd: false,
outOsd: false,
osdid: '',
osdhost: '',
},
},
controller: {
xclass: 'Ext.app.ViewController',
reload: function () {
let me = this;
let view = me.getView();
let vm = me.getViewModel();
let nodename = vm.get('nodename');
let sm = view.getSelectionModel();
Proxmox.Utils.API2Request({
url: '/nodes/' + nodename + '/ceph/osd',
waitMsgTarget: view,
method: 'GET',
failure: function (response, opts) {
let msg = response.htmlStatus;
PVE.Utils.showCephInstallOrMask(view, msg, nodename, (win) =>
view.mon(win, 'cephInstallWindowClosed', () => {
me.reload();
}),
);
},
success: function (response, opts) {
let data = response.result.data;
let selected = view.getSelection();
let name;
if (selected.length) {
name = selected[0].data.name;
}
data.versions = data.versions || {};
vm.set('versions', data.versions);
// extract max version
let maxversion = '0';
let mixedversions = false;
let traverse;
traverse = function (node, fn) {
fn(node);
if (Array.isArray(node.children)) {
node.children.forEach((c) => {
traverse(c, fn);
});
}
};
traverse(data.root, (node) => {
// compatibility for old api call
if (node.type === 'host' && !node.version) {
node.version = data.versions[node.name];
}
if (node.version === undefined) {
return;
}
if (
PVE.Utils.compare_ceph_versions(node.version, maxversion) !== 0 &&
maxversion !== '0'
) {
mixedversions = true;
}
if (PVE.Utils.compare_ceph_versions(node.version, maxversion) > 0) {
maxversion = node.version;
}
});
vm.set('maxversion', maxversion);
vm.set('mixedversions', mixedversions);
sm.deselectAll();
view.setRootNode(data.root);
view.expandAll();
if (name) {
let node = view.getRootNode().findChild('name', name, true);
if (node) {
view.setSelection([node]);
}
}
let flags = data.flags.split(',');
vm.set('flags', flags);
},
});
},
osd_cmd: function (comp) {
let me = this;
let vm = this.getViewModel();
let cmd = comp.cmd;
let params = comp.params || {};
let osdid = vm.get('osdid');
let doRequest = function () {
let targetnode = vm.get('osdhost');
// cmds not node specific and need to work if the OSD node is down
if (['in', 'out'].includes(cmd)) {
targetnode = vm.get('nodename');
}
Proxmox.Utils.API2Request({
url: `/nodes/${targetnode}/ceph/osd/${osdid}/${cmd}`,
waitMsgTarget: me.getView(),
method: 'POST',
params: params,
success: () => {
me.reload();
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
};
if (cmd === 'scrub') {
Ext.MessageBox.defaultButton = params.deep === 1 ? 2 : 1;
Ext.Msg.show({
title: gettext('Confirm'),
icon: params.deep === 1 ? Ext.Msg.WARNING : Ext.Msg.QUESTION,
msg:
params.deep !== 1
? Ext.String.format(gettext('Scrub OSD.{0}'), osdid)
: Ext.String.format(gettext('Deep Scrub OSD.{0}'), osdid) +
'
Caution: This can reduce performance while it is running.',
buttons: Ext.Msg.YESNO,
callback: function (btn) {
if (btn !== 'yes') {
return;
}
doRequest();
},
});
} else {
doRequest();
}
},
create_osd: function () {
let me = this;
let vm = this.getViewModel();
Ext.create('PVE.CephCreateOsd', {
nodename: vm.get('nodename'),
taskDone: () => {
me.reload();
},
}).show();
},
destroy_osd: async function () {
let me = this;
let vm = this.getViewModel();
let warnings = {
flags: false,
degraded: false,
};
let flagsPromise = Proxmox.Async.api2({
url: `/cluster/ceph/flags`,
method: 'GET',
});
let statusPromise = Proxmox.Async.api2({
url: `/cluster/ceph/status`,
method: 'GET',
});
me.getView().mask(gettext('Loading...'));
try {
let result = await Promise.all([flagsPromise, statusPromise]);
let flagsData = result[0].result.data;
let statusData = result[1].result.data;
let flags = Array.from(
flagsData.filter((v) => v.value),
(v) => v.name,
).filter((v) => ['norebalance', 'norecover', 'noout'].includes(v));
if (flags.length) {
warnings.flags = true;
}
if (Object.keys(statusData.pgmap).includes('degraded_objects')) {
warnings.degraded = true;
}
} catch (error) {
Ext.Msg.alert(gettext('Error'), error.htmlStatus);
me.getView().unmask();
return;
}
me.getView().unmask();
Ext.create('PVE.CephRemoveOsd', {
nodename: vm.get('osdhost'),
osdid: vm.get('osdid'),
warnings: warnings,
taskDone: () => {
me.reload();
},
autoShow: true,
});
},
set_flags: function () {
let me = this;
let vm = this.getViewModel();
Ext.create('PVE.CephSetFlags', {
nodename: vm.get('nodename'),
taskDone: () => {
me.reload();
},
}).show();
},
bulk_restart_osds: function () {
let me = this;
let vm = this.getViewModel();
let nodename = vm.get('nodename');
// Walk the full node tree to count OSDs per host; cascadeBy descends collapsed
// nodes too, which a flat store iteration would skip. Each OSD record's tree path
// includes its parent host bucket (the CRUSH tree's typical layout), so we walk
// up parentNode until we find a host entry.
let osdsByHost = {};
me.getView()
.getRootNode()
.cascadeBy(function (rec) {
if (rec.data.type !== 'osd') {
return;
}
let p = rec.parentNode;
while (p && p.data.type !== 'host') {
p = p.parentNode;
}
if (p) {
let h = p.data.name;
osdsByHost[h] = (osdsByHost[h] || 0) + 1;
}
});
if (!Object.keys(osdsByHost).length) {
Ext.Msg.alert(
gettext('No OSDs'),
gettext('No OSDs are configured in the cluster. Nothing to restart.'),
);
return;
}
Ext.create('PVE.window.CephBulkRestartOSDs', {
autoShow: true,
osdsByHost,
nodename,
onTaskStarted: (upid) => {
Ext.create('Proxmox.window.TaskProgress', {
autoShow: true,
upid,
taskDone: () => me.reload(),
});
},
});
},
service_cmd: function (comp) {
let me = this;
let vm = this.getViewModel();
let cmd = comp.cmd || comp;
let doRequest = function () {
Proxmox.Utils.API2Request({
url: `/nodes/${vm.get('osdhost')}/ceph/${cmd}`,
params: { service: 'osd.' + vm.get('osdid') },
waitMsgTarget: me.getView(),
method: 'POST',
success: function (response, options) {
let upid = response.result.data;
let win = Ext.create('Proxmox.window.TaskProgress', {
upid: upid,
taskDone: () => {
me.reload();
},
});
win.show();
},
failure: function (response, opts) {
Ext.Msg.alert(gettext('Error'), response.htmlStatus);
},
});
};
if (cmd === 'stop') {
Proxmox.Utils.API2Request({
url: `/nodes/${vm.get('osdhost')}/ceph/cmd-safety`,
params: {
service: 'osd',
id: vm.get('osdid'),
action: 'stop',
},
waitMsgTarget: me.getView(),
method: 'GET',
success: function ({ result: { data } }) {
if (!data.safe) {
Ext.Msg.show({
title: ngettext('Warning', 'Warnings', 1),
message: data.status,
icon: Ext.Msg.WARNING,
buttons: Ext.Msg.OKCANCEL,
buttonText: { ok: gettext('Stop OSD') },
fn: function (selection) {
if (selection === 'ok') {
doRequest();
}
},
});
} else {
doRequest();
}
},
failure: (response) => Ext.Msg.alert(gettext('Error'), response.htmlStatus),
});
} else {
doRequest();
}
},
run_details: function (view, rec) {
if (rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0) {
this.details();
}
},
details: function () {
let vm = this.getViewModel();
Ext.create('PVE.CephOsdDetails', {
nodename: vm.get('osdhost'),
osdid: vm.get('osdid'),
}).show();
},
set_selection_status: function (tp, selection) {
if (selection.length < 1) {
return;
}
let rec = selection[0];
let vm = this.getViewModel();
let isOsd = rec.data.host && rec.data.type === 'osd' && rec.data.id >= 0;
vm.set('isOsd', isOsd);
vm.set('downOsd', isOsd && rec.data.status === 'down');
vm.set('upOsd', isOsd && rec.data.status !== 'down');
vm.set('inOsd', isOsd && rec.data.in);
vm.set('outOsd', isOsd && !rec.data.in);
vm.set('osdid', isOsd ? rec.data.id : undefined);
vm.set('osdhost', isOsd ? rec.data.host : undefined);
},
render_status: function (value, metaData, rec) {
if (!value) {
return value;
}
let inout = rec.data.in ? 'in' : 'out';
let updownicon =
value === 'up' ? 'good fa-arrow-circle-up' : 'critical fa-arrow-circle-down';
let inouticon = rec.data.in ? 'good fa-circle' : 'warning fa-circle-o';
let text =
value +
' / ' +
inout +
' ';
return text;
},
render_wal: function (value, metaData, rec) {
if (!value && rec.data.osdtype === 'bluestore' && rec.data.type === 'osd') {
return 'N/A';
}
return value;
},
render_version: function (value, metadata, rec) {
let vm = this.getViewModel();
let versions = vm.get('versions');
let icon = '';
let version = value || '';
let maxversion = vm.get('maxversion');
if (value && PVE.Utils.compare_ceph_versions(value, maxversion) !== 0) {
let host_version = rec.parentNode?.data?.version || versions[rec.data.host] || '';
if (
rec.data.type === 'host' ||
PVE.Utils.compare_ceph_versions(host_version, maxversion) !== 0
) {
icon = PVE.Utils.get_ceph_icon_html('HEALTH_UPGRADE');
} else {
icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
}
} else if (value && vm.get('mixedversions')) {
icon = PVE.Utils.get_ceph_icon_html('HEALTH_OK');
}
return icon + version;
},
render_osd_val: function (value, metaData, rec) {
return rec.data.type === 'osd' ? value : '';
},
render_osd_weight: function (value, metaData, rec) {
if (rec.data.type !== 'osd') {
return '';
}
return Ext.util.Format.number(value, '0.00###');
},
render_osd_latency: function (value, metaData, rec) {
if (rec.data.type !== 'osd') {
return '';
}
let commit_ms = rec.data.commit_latency_ms,
apply_ms = rec.data.apply_latency_ms;
return apply_ms + ' / ' + commit_ms;
},
render_osd_size: function (value, metaData, rec) {
return this.render_osd_val(Proxmox.Utils.render_size(value), metaData, rec);
},
control: {
'#': {
selectionchange: 'set_selection_status',
},
},
init: function (view) {
let me = this;
let vm = this.getViewModel();
if (!view.pveSelNode.data.node) {
throw 'no node name specified';
}
vm.set('nodename', view.pveSelNode.data.node);
me.callParent();
me.reload();
},
},
stateful: true,
stateId: 'grid-ceph-osd',
rootVisible: false,
useArrows: true,
listeners: {
itemdblclick: 'run_details',
},
columns: [
{
xtype: 'treecolumn',
text: 'Name',
dataIndex: 'name',
width: 150,
},
{
text: 'Type',
dataIndex: 'type',
hidden: true,
align: 'right',
width: 75,
},
{
text: gettext('Class'),
dataIndex: 'device_class',
align: 'right',
width: 75,
renderer: Ext.htmlEncode,
},
{
text: 'OSD Type',
dataIndex: 'osdtype',
align: 'right',
width: 100,
},
{
text: 'Bluestore Device',
dataIndex: 'blfsdev',
align: 'right',
width: 75,
hidden: true,
renderer: Ext.htmlEncode,
},
{
text: 'DB Device',
dataIndex: 'dbdev',
align: 'right',
width: 75,
hidden: true,
renderer: Ext.htmlEncode,
},
{
text: 'WAL Device',
dataIndex: 'waldev',
align: 'right',
renderer: 'render_wal',
width: 75,
hidden: true,
},
{
text: 'Status',
dataIndex: 'status',
align: 'right',
renderer: 'render_status',
width: 120,
},
{
text: gettext('Version'),
dataIndex: 'version',
align: 'right',
renderer: 'render_version',
},
{
text: 'weight',
dataIndex: 'crush_weight',
align: 'right',
renderer: 'render_osd_weight',
width: 90,
},
{
text: 'reweight',
dataIndex: 'reweight',
align: 'right',
renderer: 'render_osd_weight',
width: 90,
},
{
text: gettext('Used') + ' (%)',
dataIndex: 'percent_used',
align: 'right',
renderer: function (value, metaData, rec) {
if (rec.data.type !== 'osd') {
return '';
}
return Ext.util.Format.number(value, '0.00');
},
width: 100,
},
{
text: gettext('Total'),
dataIndex: 'total_space',
align: 'right',
renderer: 'render_osd_size',
width: 100,
},
{
text: 'Apply/Commit
Latency (ms)',
dataIndex: 'apply_latency_ms',
align: 'right',
renderer: 'render_osd_latency',
width: 120,
},
{
text: 'PGs',
dataIndex: 'pgs',
align: 'right',
renderer: 'render_osd_val',
width: 90,
},
],
tbar: {
overflowHandler: 'scroller',
items: [
{
text: gettext('Reload'),
iconCls: 'fa fa-refresh',
handler: 'reload',
},
'-',
{
text: gettext('Create: OSD'),
handler: 'create_osd',
},
{
text: gettext('Manage Global Flags'),
handler: 'set_flags',
},
{
text: gettext('Bulk Restart OSDs'),
iconCls: 'fa fa-refresh',
handler: 'bulk_restart_osds',
},
'->',
{
xtype: 'tbtext',
data: {
osd: undefined,
},
bind: {
data: {
osd: '{osdid}',
},
},
tpl: [
'',
'osd.{osd}:',
'',
gettext('No OSD selected'),
'',
],
},
{
text: gettext('Details'),
iconCls: 'fa fa-info-circle',
disabled: true,
bind: {
disabled: '{!isOsd}',
},
handler: 'details',
},
{
text: gettext('Start'),
iconCls: 'fa fa-play',
disabled: true,
bind: {
disabled: '{!downOsd}',
},
cmd: 'start',
handler: 'service_cmd',
},
{
text: gettext('Stop'),
iconCls: 'fa fa-stop',
disabled: true,
bind: {
disabled: '{!upOsd}',
},
cmd: 'stop',
handler: 'service_cmd',
},
{
text: gettext('Restart'),
iconCls: 'fa fa-refresh',
disabled: true,
bind: {
disabled: '{!upOsd}',
},
cmd: 'restart',
handler: 'service_cmd',
},
'-',
{
text: 'Out',
iconCls: 'fa fa-circle-o',
disabled: true,
bind: {
disabled: '{!inOsd}',
},
cmd: 'out',
handler: 'osd_cmd',
},
{
text: 'In',
iconCls: 'fa fa-circle',
disabled: true,
bind: {
disabled: '{!outOsd}',
},
cmd: 'in',
handler: 'osd_cmd',
},
'-',
{
text: gettext('More'),
iconCls: 'fa fa-bars',
disabled: true,
bind: {
disabled: '{!isOsd}',
},
menu: [
{
text: gettext('Scrub'),
iconCls: 'fa fa-shower',
cmd: 'scrub',
handler: 'osd_cmd',
},
{
text: gettext('Deep Scrub'),
iconCls: 'fa fa-bath',
cmd: 'scrub',
params: {
deep: 1,
},
handler: 'osd_cmd',
},
{
text: gettext('Destroy'),
itemId: 'remove',
iconCls: 'fa fa-fw fa-trash-o',
bind: {
disabled: '{!downOsd}',
},
handler: 'destroy_osd',
},
],
},
],
},
fields: [
'name',
'type',
'status',
'host',
'in',
'id',
{ type: 'number', name: 'reweight' },
{ type: 'number', name: 'percent_used' },
{ type: 'integer', name: 'bytes_used' },
{ type: 'integer', name: 'total_space' },
{ type: 'integer', name: 'apply_latency_ms' },
{ type: 'integer', name: 'commit_latency_ms' },
{ type: 'string', name: 'device_class' },
{ type: 'string', name: 'osdtype' },
{ type: 'string', name: 'blfsdev' },
{ type: 'string', name: 'dbdev' },
{ type: 'string', name: 'waldev' },
{
type: 'string',
name: 'version',
calculate: function (data) {
return PVE.Utils.parse_ceph_version(data);
},
},
{
type: 'string',
name: 'iconCls',
calculate: function (data) {
let iconMap = {
host: 'fa-building',
osd: 'fa-hdd-o',
root: 'fa-server',
};
return `fa x-fa-tree ${iconMap[data.type] ?? 'fa-folder-o'}`;
},
},
{ type: 'number', name: 'crush_weight' },
],
});