[pve-devel] [PATCH manager 2/2] gui: ceph osd: rewrite osd panel and add scrub button
Dominik Csapak
d.csapak at proxmox.com
Wed Jul 10 15:13:52 CEST 2019
this rewrite of the osd panel converts it to a static declaration of
the layout with a controller/viewmodel logic
while doing this, add icons to buttons, and add a scrub/deep scrub entry
Signed-off-by: Dominik Csapak <d.csapak at proxmox.com>
---
i guess it would be easiest to review the result and not the diff,
since it is simply a big rewrite of the whole component..
www/manager6/ceph/OSD.js | 789 +++++++++++++++++++++------------------
1 file changed, 420 insertions(+), 369 deletions(-)
diff --git a/www/manager6/ceph/OSD.js b/www/manager6/ceph/OSD.js
index 31cd3ea3..b7c88475 100644
--- a/www/manager6/ceph/OSD.js
+++ b/www/manager6/ceph/OSD.js
@@ -167,8 +167,271 @@ Ext.define('PVE.node.CephOsdTree', {
extend: 'Ext.tree.Panel',
alias: ['widget.pveNodeCephOsdTree'],
onlineHelp: 'chapter_pveceph',
+
+ viewModel: {
+ data: {
+ nodename: '',
+ flags: [],
+ maxversion: '0',
+ versions: {},
+ isOsd: false,
+ downOsd: false,
+ upOsd: false,
+ inOsd: false,
+ outOsd: false,
+ osdid: '',
+ osdhost: '',
+ }
+ },
+
+ controller: {
+ xclass: 'Ext.app.ViewController',
+
+ reload: function() {
+ var me = this.getView();
+ var vm = this.getViewModel();
+ var nodename = vm.get('nodename');
+ var sm = me.getSelectionModel();
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + nodename + "/ceph/osd",
+ waitMsgTarget: me,
+ method: 'GET',
+ failure: function(response, opts) {
+ var msg = response.htmlStatus;
+ PVE.Utils.showCephInstallOrMask(me, msg, nodename,
+ function(win){
+ me.mon(win, 'cephInstallWindowClosed', this.reload);
+ }
+ );
+ },
+ success: function(response, opts) {
+ var data = response.result.data;
+ var selected = me.getSelection();
+ var name;
+ if (selected.length) {
+ name = selected[0].data.name;
+ }
+ vm.set('versions', data.versions);
+ // extract max version
+ var maxversion = vm.get('maxversion');
+ Object.values(data.versions || {}).forEach(function(version) {
+ if (PVE.Utils.compare_ceph_versions(version, maxversion) > 0) {
+ maxversion = version;
+ }
+ });
+ vm.set('maxversion', maxversion);
+ sm.deselectAll();
+ me.setRootNode(data.root);
+ me.expandAll();
+ if (name) {
+ var node = me.getRootNode().findChild('name', name, true);
+ if (node) {
+ me.setSelection([node]);
+ }
+ }
+
+ var flags = data.flags.split(',');
+ vm.set('flags', flags);
+ var noout = flags.includes('noout');
+ me.down('#nooutBtn').setText(noout ? gettext("Unset noout") : gettext("Set noout"));
+ }
+ });
+ },
+
+ osd_cmd: function(comp) {
+ var me = this;
+ var vm = this.getViewModel();
+ var cmd = comp.cmd;
+ var params = comp.params || {};
+ var osdid = vm.get('osdid');
+
+ var doRequest = function() {
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + vm.get('osdhost') + "/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) +
+ "<br>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() {
+ var me = this;
+ var vm = this.getViewModel();
+ Ext.create('PVE.CephCreateOsd', {
+ nodename: vm.get('nodename'),
+ taskDone: () => { me.reload(); }
+ }).show();
+ },
+
+ destroy_osd: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ Ext.create('PVE.CephRemoveOsd', {
+ nodename: vm.get('osdhost'),
+ osdid: vm.get('osdid'),
+ taskDone: () => { me.reload(); }
+ }).show();
+ },
+
+ set_flag: function() {
+ var me = this;
+ var vm = this.getViewModel();
+ var flags = vm.get('flags');
+ Proxmox.Utils.API2Request({
+ url: "/nodes/" + vm.get('nodename') + "/ceph/flags/noout",
+ waitMsgTarget: me.getView(),
+ method: flags.includes('noout') ? 'DELETE' : 'POST',
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ },
+ success: () => { me.reload(); }
+ });
+ },
+
+ service_cmd: function(comp) {
+ var me = this;
+ var vm = this.getViewModel();
+ var cmd = comp.cmd || comp;
+ 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) {
+ var upid = response.result.data;
+ var win = Ext.create('Proxmox.window.TaskProgress', {
+ upid: upid,
+ taskDone: () => { me.reload(); }
+ });
+ win.show();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ },
+
+ set_selection_status: function(tp, selection) {
+ if (selection.length < 1) {
+ return;
+ }
+ var rec = selection[0];
+ var vm = this.getViewModel();
+
+ var 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);
+ vm.notify();
+ },
+
+ render_status: function(value, metaData, rec) {
+ if (!value) {
+ return value;
+ }
+ var inout = rec.data['in'] ? 'in' : 'out';
+ var updownicon = value === 'up' ? 'good fa-arrow-circle-up' :
+ 'critical fa-arrow-circle-down';
+
+ var inouticon = rec.data['in'] ? 'good fa-circle' :
+ 'warning fa-circle-o';
+
+ var text = value + ' <i class="fa ' + updownicon + '"></i> / ' +
+ inout + ' <i class="fa ' + inouticon + '"></i>';
+
+ 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) {
+ var vm = this.getViewModel();
+ var versions = vm.get('versions');
+ var icon = "";
+ var version = value || "";
+ if (value && value != vm.get('maxversion')) {
+ icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
+ }
+
+ if (!value && rec.data.type == 'host') {
+ version = versions[rec.data.name] || Proxmox.Utils.unknownText;
+ }
+
+ return icon + version;
+ },
+
+ render_osd_val: function(value, metaData, rec) {
+ return (rec.data.type === 'osd') ? value : '';
+ },
+
+ render_osd_size: function(value, metaData, rec) {
+ return this.render_osd_val(PVE.Utils.render_size(value), metaData, rec);
+ },
+
+ control: {
+ '#': {
+ selectionchange: 'set_selection_status'
+ }
+ },
+
+ init: function(view) {
+ var me = this;
+ var 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,
+
columns: [
{
xtype: 'treecolumn',
@@ -212,14 +475,7 @@ Ext.define('PVE.node.CephOsdTree', {
text: "WAL Device",
dataIndex: 'waldev',
align: 'right',
- renderer: function(value, metaData, rec) {
- if (!value &&
- rec.data.osdtype === 'bluestore' &&
- rec.data.type === 'osd') {
- return 'N/A';
- }
- return value;
- },
+ renderer: 'render_wal',
width: 40,
hidden: true
},
@@ -227,94 +483,34 @@ Ext.define('PVE.node.CephOsdTree', {
text: 'Status',
dataIndex: 'status',
align: 'right',
- renderer: function(value, metaData, rec) {
- if (!value) {
- return value;
- }
- var inout = rec.data['in'] ? 'in' : 'out';
- var updownicon = value === 'up' ? 'good fa-arrow-circle-up' :
- 'critical fa-arrow-circle-down';
-
- var inouticon = rec.data['in'] ? 'good fa-circle' :
- 'warning fa-circle-o';
-
- var text = value + ' <i class="fa ' + updownicon + '"></i> / ' +
- inout + ' <i class="fa ' + inouticon + '"></i>';
-
- return text;
- },
+ renderer: 'render_status',
width: 80
},
{
text: gettext('Version'),
dataIndex: 'version',
- renderer: function(value, metadata, rec) {
- var me = this;
- var icon = "";
- var version = value || "";
- if (value && value != me.maxversion) {
- icon = PVE.Utils.get_ceph_icon_html('HEALTH_OLD');
- }
-
- if (!value && rec.data.type == 'host') {
- version = me.versions[rec.data.name] || Proxmox.Utils.unknownText;
- }
-
- return icon + version;
- }
+ renderer: 'render_version'
},
{
text: 'weight',
dataIndex: 'crush_weight',
align: 'right',
- renderer: function(value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return value;
- },
+ renderer: 'render_osd_val',
width: 80
},
{
text: 'reweight',
dataIndex: 'reweight',
align: 'right',
- renderer: function(value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return value;
- },
+ renderer: 'render_osd_val',
width: 90
},
{
- header: gettext('Used'),
- columns: [
- {
- text: '%',
- 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: 80
- },
- {
- text: gettext('Total'),
- dataIndex: 'total_space',
- align: 'right',
- renderer: function(value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return PVE.Utils.render_size(value);
- },
- width: 100
- }
- ]
+ text: gettext('Total'),
+ dataIndex: 'total_space',
+ align: 'right',
+ renderer: 'render_osd_size',
+ width: 100
},
{
header: gettext('Latency (ms)'),
@@ -323,314 +519,169 @@ Ext.define('PVE.node.CephOsdTree', {
text: 'Apply',
dataIndex: 'apply_latency_ms',
align: 'right',
- renderer: function(value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return value;
- },
+ renderer: 'render_osd_val',
width: 60
},
{
text: 'Commit',
dataIndex: 'commit_latency_ms',
align: 'right',
- renderer: function(value, metaData, rec) {
- if (rec.data.type !== 'osd') {
- return '';
- }
- return value;
- },
+ renderer: 'render_osd_val',
width: 60
}
]
}
],
- initComponent: function() {
- /*jslint confusion: true */
- var me = this;
-
- // we expect noout to be not set by default
- var noout = false;
- me.maxversion = "0";
-
- var nodename = me.pveSelNode.data.node;
- if (!nodename) {
- throw "no node name specified";
- }
-
- var sm = Ext.create('Ext.selection.TreeModel', {});
- var set_button_status; // defined later
- var reload = function() {
- Proxmox.Utils.API2Request({
- url: "/nodes/" + nodename + "/ceph/osd",
- waitMsgTarget: me,
- method: 'GET',
- failure: function(response, opts) {
- var msg = response.htmlStatus;
- PVE.Utils.showCephInstallOrMask(me, msg, me.pveSelNode.data.node,
- function(win){
- me.mon(win, 'cephInstallWindowClosed', function(){
- reload();
- });
- }
- );
+ tbar: {
+ items: [
+ {
+ text: gettext('Reload'),
+ iconCls: 'fa fa-refresh',
+ handler: 'reload'
+ },
+ {
+ text: gettext('Create') + ': OSD',
+ handler: 'create_osd',
+ },
+ {
+ text: gettext('Set noout'),
+ itemId: 'nooutBtn',
+ handler: 'set_flag',
+ },
+ '->',
+ {
+ xtype: 'tbtext',
+ data: {
+ osd: undefined
},
- success: function(response, opts) {
- var data = response.result.data;
- var selected = me.getSelection();
- var name;
- if (selected.length) {
- name = selected[0].data.name;
- }
- sm.deselectAll();
- me.setRootNode(data.root);
- me.expandAll();
- if (name) {
- var node = me.getRootNode().findChild('name', name, true);
- if (node) {
- me.setSelection([node]);
- }
+ bind: {
+ data: {
+ osd: "{osdid}"
}
- // extract noout flag
- if (data.flags && data.flags.search(/noout/) !== -1) {
- noout = true;
- } else {
- noout = false;
- }
-
- me.versions = data.versions;
- // extract max version
- Object.values(data.versions || {}).forEach(function(version) {
- if (PVE.Utils.compare_ceph_versions(version, me.maxversion) > 0) {
- me.maxversion = version;
- }
- });
- set_button_status();
- }
- });
- };
-
- var osd_cmd = function(cmd) {
- var rec = sm.getSelection()[0];
- if (!(rec && (rec.data.id >= 0) && rec.data.host)) {
- return;
- }
- Proxmox.Utils.API2Request({
- url: "/nodes/" + rec.data.host + "/ceph/osd/" +
- rec.data.id + '/' + cmd,
- waitMsgTarget: me,
- method: 'POST',
- success: reload,
- failure: function(response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- }
- });
- };
-
- var service_cmd = function(cmd) {
- var rec = sm.getSelection()[0];
- if (!(rec && rec.data.name && rec.data.host)) {
- return;
- }
- Proxmox.Utils.API2Request({
- url: "/nodes/" + rec.data.host + "/ceph/" + cmd,
- params: { service: rec.data.name },
- waitMsgTarget: me,
- method: 'POST',
- success: function(response, options) {
- var upid = response.result.data;
- var win = Ext.create('Proxmox.window.TaskProgress', {
- upid: upid,
- taskDone: function() {
- reload();
- }
- });
- win.show();
},
- failure: function(response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- }
- });
- };
-
- var create_btn = new Proxmox.button.Button({
- text: gettext('Create') + ': OSD',
- handler: function() {
- var rec = sm.getSelection()[0];
-
- var win = Ext.create('PVE.CephCreateOsd', {
- nodename: nodename,
- taskDone: function(success) {
- reload();
- }
- });
- win.show();
- }
- });
-
- var start_btn = new Ext.Button({
- text: gettext('Start'),
- disabled: true,
- handler: function(){ service_cmd('start'); }
- });
-
- var stop_btn = new Ext.Button({
- text: gettext('Stop'),
- disabled: true,
- handler: function(){ service_cmd('stop'); }
- });
-
- var restart_btn = new Ext.Button({
- text: gettext('Restart'),
- disabled: true,
- handler: function(){ service_cmd('restart'); }
- });
-
- var osd_out_btn = new Ext.Button({
- text: 'Out',
- disabled: true,
- handler: function(){ osd_cmd('out'); }
- });
-
- var osd_in_btn = new Ext.Button({
- text: 'In',
- disabled: true,
- handler: function(){ osd_cmd('in'); }
- });
-
- var remove_btn = new Ext.Button({
- text: gettext('Destroy'),
- disabled: true,
- handler: function(){
- var rec = sm.getSelection()[0];
- if (!(rec && (rec.data.id >= 0) && rec.data.host)) {
- return;
- }
-
- var win = Ext.create('PVE.CephRemoveOsd', {
- nodename: rec.data.host,
- osdid: rec.data.id,
- taskDone: function(success) {
- reload();
- }
- });
- win.show();
- }
- });
-
- var noout_btn = new Ext.Button({
- text: gettext('Set noout'),
- handler: function() {
- Proxmox.Utils.API2Request({
- url: "/nodes/" + nodename + "/ceph/flags/noout",
- waitMsgTarget: me,
- method: noout ? 'DELETE' : 'POST',
- failure: function(response, opts) {
- Ext.Msg.alert(gettext('Error'), response.htmlStatus);
- },
- success: reload
- });
- }
- });
-
- var osd_label = new Ext.toolbar.TextItem({
- data: {
- osd: undefined
+ tpl: [
+ '<tpl if="osd">',
+ 'osd.{osd}:',
+ '<tpl else>',
+ gettext('No OSD selected'),
+ '</tpl>'
+ ]
},
- tpl: [
- '<tpl if="osd">',
- '{osd}:',
- '<tpl else>',
- gettext('No OSD selected'),
- '</tpl>'
- ]
- });
-
- set_button_status = function() {
- var rec = sm.getSelection()[0];
- noout_btn.setText(noout?gettext('Unset noout'):gettext('Set noout'));
-
- if (!rec) {
- start_btn.setDisabled(true);
- stop_btn.setDisabled(true);
- restart_btn.setDisabled(true);
- remove_btn.setDisabled(true);
- osd_out_btn.setDisabled(true);
- osd_in_btn.setDisabled(true);
- return;
- }
-
- var isOsd = (rec.data.host && (rec.data.type === 'osd') && (rec.data.id >= 0));
-
- start_btn.setDisabled(!(isOsd && (rec.data.status !== 'up')));
- stop_btn.setDisabled(!(isOsd && (rec.data.status !== 'down')));
- restart_btn.setDisabled(!(isOsd && (rec.data.status !== 'down')));
- remove_btn.setDisabled(!(isOsd && (rec.data.status === 'down')));
-
- osd_out_btn.setDisabled(!(isOsd && rec.data['in']));
- osd_in_btn.setDisabled(!(isOsd && !rec.data['in']));
-
- osd_label.update(isOsd?{osd:rec.data.name}:undefined);
- };
-
- sm.on('selectionchange', set_button_status);
-
- var reload_btn = new Ext.Button({
- text: gettext('Reload'),
- handler: reload
- });
-
- Ext.apply(me, {
- tbar: [ create_btn, reload_btn, noout_btn, '->', osd_label, start_btn, stop_btn, restart_btn, osd_out_btn, osd_in_btn, remove_btn ],
- rootVisible: false,
- useArrows: true,
- 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) {
- var iconCls = 'fa x-fa-tree fa-';
- switch (data.type) {
- case 'host':
- iconCls += 'building';
- break;
- case 'osd':
- iconCls += 'hdd-o';
- break;
- case 'root':
- iconCls += 'server';
- break;
- default:
- return undefined;
- }
- return iconCls;
- } },
- { type: 'number', name: 'crush_weight' }],
- selModel: sm,
-
- listeners: {
- activate: function() {
- reload();
- }
+ {
+ 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('Actions'),
+ 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'
+ }
+ ],
}
- });
-
- me.callParent();
-
- reload();
- }
+ ]
+ },
+
+ 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) {
+ var iconMap = {
+ host: 'fa-building',
+ osd: 'fa-hdd-o',
+ root: 'fa-server',
+ };
+ return 'fa x-fa-tree ' + iconMap[data.type];
+ } },
+ { type: 'number', name: 'crush_weight' }
+ ],
});
--
2.20.1
More information about the pve-devel
mailing list