Add components for creating/updating custom CPU models, allowing to set
the values that would normally have to be set manually in
`/etc/pve/virtual-guest/cpu-models.conf` [0].

Original patch:
https://lore.proxmox.com/pve-devel/[email protected]/

[0] https://pve.proxmox.com/wiki/Manual:_cpu-models.conf

Originally-by: Stefan Reiter <[email protected]>
Signed-off-by: Arthur Bied-Charreton <[email protected]>
---
 www/manager6/Makefile                 |   3 +
 www/manager6/dc/CPUTypeEdit.js        |  90 +++++++++++++++
 www/manager6/dc/CPUTypeView.js        | 139 +++++++++++++++++++++++
 www/manager6/dc/Config.js             |   6 +
 www/manager6/form/PhysBitsSelector.js | 153 ++++++++++++++++++++++++++
 5 files changed, 391 insertions(+)
 create mode 100644 www/manager6/dc/CPUTypeEdit.js
 create mode 100644 www/manager6/dc/CPUTypeView.js
 create mode 100644 www/manager6/form/PhysBitsSelector.js

diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 4558d53e..fbde2327 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -58,6 +58,7 @@ JSSRC=                                                        
\
        form/PCISelector.js                             \
        form/PCIMapSelector.js                          \
        form/PermPathSelector.js                        \
+       form/PhysBitsSelector.js                        \
        form/PoolSelector.js                            \
        form/PreallocationSelector.js                   \
        form/PrivilegesSelector.js                      \
@@ -170,6 +171,8 @@ JSSRC=                                                      
\
        dc/CmdMenu.js                                   \
        dc/Config.js                                    \
        dc/CorosyncLinkEdit.js                          \
+       dc/CPUTypeEdit.js                               \
+       dc/CPUTypeView.js                               \
        dc/GroupEdit.js                                 \
        dc/GroupView.js                                 \
        dc/Guests.js                                    \
diff --git a/www/manager6/dc/CPUTypeEdit.js b/www/manager6/dc/CPUTypeEdit.js
new file mode 100644
index 00000000..8cf508b4
--- /dev/null
+++ b/www/manager6/dc/CPUTypeEdit.js
@@ -0,0 +1,90 @@
+Ext.define('PVE.dc.CPUTypeEdit', {
+    extend: 'Proxmox.window.Edit',
+    alias: ['widget.pveCpuTypeEdit'],
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    subject: gettext('CPU Type'),
+
+    // Avoid default-focusing the reported model dropdown while still
+    // focusing the name textfield if it is editable
+    defaultFocus: 'textfield',
+
+    height: 600,
+    width: 800,
+
+    cbindData: {
+        cputype: '',
+        isCreate: (cfg) => !cfg.cputype,
+    },
+
+    cbind: {
+        autoLoad: (get) => !get('isCreate'),
+        url: (get) => 
`/api2/extjs/nodes/localhost/capabilities/qemu/cpu/model/${get('cputype')}`,
+        method: (get) => (get('isCreate') ? 'POST' : 'PUT'),
+        isCreate: (get) => get('isCreate'),
+    },
+
+    getValues: function () {
+        let me = this;
+        let values = me.callParent();
+
+        PVE.Utils.delete_if_default(values, 'reported-model', '', me.isCreate);
+        PVE.Utils.delete_if_default(values, 'hv-vendor-id', '', me.isCreate);
+        PVE.Utils.delete_if_default(values, 'phys-bits', '', me.isCreate);
+        PVE.Utils.delete_if_default(values, 'flags', '', me.isCreate);
+
+        if (me.isCreate && !values.cputype.match(/^custom-/)) {
+            values.cputype = 'custom-' + values.cputype;
+        }
+
+        return values;
+    },
+
+    items: [
+        {
+            xtype: 'inputpanel',
+            column1: [
+                {
+                    xtype: 'pmxDisplayEditField',
+                    fieldLabel: gettext('Name'),
+                    cbind: {
+                        editable: '{isCreate}',
+                        value: '{cputype}',
+                    },
+                    name: 'cputype',
+                    renderer: (val) => val.replace(/^custom-/, ''),
+                    allowBlank: false,
+                },
+                {
+                    xtype: 'CPUModelSelector',
+                    fieldLabel: gettext('Reported Model'),
+                    allowCustom: false,
+                    name: 'reported-model',
+                },
+                {
+                    xtype: 'textfield',
+                    fieldLabel: gettext('Hyper-V Vendor'),
+                    name: 'hv-vendor-id',
+                    allowBlank: true,
+                    emptyText: gettext('None'),
+                    maxLength: 12,
+                },
+            ],
+            column2: [
+                {
+                    xtype: 'checkbox',
+                    fieldLabel: gettext('Hidden'),
+                    name: 'hidden',
+                    inputValue: 1,
+                    uncheckedValue: 0,
+                },
+                {
+                    xtype: 'PhysBitsSelector',
+                    fieldLabel: gettext('Phys-Bits'),
+                    name: 'phys-bits',
+                },
+            ],
+
+        },
+    ],
+});
diff --git a/www/manager6/dc/CPUTypeView.js b/www/manager6/dc/CPUTypeView.js
new file mode 100644
index 00000000..c79ce690
--- /dev/null
+++ b/www/manager6/dc/CPUTypeView.js
@@ -0,0 +1,139 @@
+Ext.define('pve-custom-cpu-type', {
+    extend: 'Ext.data.Model',
+    fields: [
+        'cputype',
+        'reported-model',
+        'hv-vendor-id',
+        'flags',
+        'phys-bits',
+        { name: 'hidden', type: 'boolean' },
+    ],
+});
+
+Ext.define('PVE.dc.CPUTypeView', {
+    extend: 'Ext.grid.GridPanel',
+    alias: ['widget.pveCPUTypeView'],
+
+    onlineHelp: 'qm_cpu',
+
+    store: {
+        model: 'pve-custom-cpu-type',
+        proxy: {
+            type: 'proxmox',
+            url: '/api2/json/nodes/localhost/capabilities/qemu/cpu/model',
+        },
+        autoLoad: true,
+        sorters: ['cputype'],
+    },
+
+    controller: {
+        xclass: 'Ext.app.ViewController',
+
+        getSelection: function () {
+            let me = this;
+            let grid = me.getView();
+            let selection = grid.getSelection();
+            if (selection.length === 1) {
+                return selection[0].data;
+            }
+            return null;
+        },
+
+        showEditor: function (cputype) {
+            let me = this;
+            let param = cputype ? { cputype } : {};
+            let win = Ext.create('PVE.dc.CPUTypeEdit', param);
+            win.on('destroy', () => me.reload());
+            win.show();
+        },
+
+        onAdd: function () {
+            let me = this;
+            me.showEditor();
+        },
+
+        onEdit: function () {
+            let me = this;
+            let selection = me.getSelection();
+            me.showEditor(selection.cputype);
+        },
+
+        reload: function () {
+            let me = this;
+            me.getView().getStore().reload();
+        },
+    },
+
+    columns: [
+        {
+            header: gettext('Name'),
+            flex: 1,
+            dataIndex: 'cputype',
+            renderer: (val) => val.replace(/^custom-/, ''),
+        },
+        {
+            header: gettext('Reported Model'),
+            flex: 1,
+            dataIndex: 'reported-model',
+        },
+        {
+            header: gettext('Phys-Bits'),
+            flex: 1,
+            dataIndex: 'phys-bits',
+        },
+        {
+            header: gettext('Hidden'),
+            flex: 1,
+            dataIndex: 'hidden',
+        },
+        {
+            header: gettext('HyperV-Vendor'),
+            flex: 1,
+            dataIndex: 'hv-vendor-id',
+        },
+        {
+            header: gettext('Flags'),
+            flex: 2,
+            dataIndex: 'flags',
+        },
+    ],
+
+    tbar: [
+        {
+            text: gettext('Add'),
+            handler: 'onAdd',
+        },
+        '-',
+        {
+            xtype: 'proxmoxStdRemoveButton',
+            baseurl: 
'/api2/extjs/nodes/localhost/capabilities/qemu/cpu/model/',
+            getRecordName: (rec) => rec.data.cputype,
+            getUrl: function (rec) {
+                let me = this;
+                return me.baseurl + rec.data.cputype;
+            },
+            callback: 'reload',
+        },
+        {
+            text: gettext('Edit'),
+            handler: 'onEdit',
+        },
+    ],
+
+    selModel: {
+        xtype: 'rowmodel',
+    },
+
+    listeners: {
+        itemdblclick: function (_, rec) {
+            let me = this;
+            me.getController().showEditor(rec.data.cputype);
+        },
+    },
+
+    initComponent: function () {
+        let me = this;
+        me.callParent();
+        Proxmox.Utils.monStoreErrors(me, me.store);
+    },
+});
diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
index b5e27a21..629e4fc8 100644
--- a/www/manager6/dc/Config.js
+++ b/www/manager6/dc/Config.js
@@ -146,6 +146,12 @@ Ext.define('PVE.dc.Config', {
                     title: gettext('Replication'),
                     itemId: 'replication',
                 },
+                {
+                    xtype: 'pveCPUTypeView',
+                    iconCls: 'fa fa-microchip',
+                    title: gettext('Custom CPU models'),
+                    itemId: 'cputypes',
+                },
                 {
                     xtype: 'pveACLView',
                     title: gettext('Permissions'),
diff --git a/www/manager6/form/PhysBitsSelector.js 
b/www/manager6/form/PhysBitsSelector.js
new file mode 100644
index 00000000..78f932a6
--- /dev/null
+++ b/www/manager6/form/PhysBitsSelector.js
@@ -0,0 +1,153 @@
+Ext.define('PVE.form.PhysBitsSelector', {
+    extend: 'Ext.form.FieldContainer',
+    alias: 'widget.PhysBitsSelector',
+    mixins: ['Ext.form.field.Field'],
+
+    layout: 'vbox',
+    originalValue: '',
+
+    controller: {
+        xclass: 'Ext.app.ViewController',
+
+        onRadioChange: function (radio, value) {
+            let me = this;
+            if (value === undefined) {
+                return;
+            }
+
+            ['modeDefault', 'modeHost', 'modeCustom'].forEach(function (ref) {
+                let r = me.lookupReference(ref);
+                if (r !== radio) {
+                    r.suspendEvents();
+                    r.setValue(false);
+                    r.resumeEvents();
+                }
+            });
+
+            me.updateNumberField();
+        },
+
+        updateNumberField: function () {
+            let me = this;
+            let modeCustom = me.lookupReference('modeCustom');
+            let customNum = me.lookupReference('customNum');
+
+            customNum.setDisabled(!modeCustom.getValue());
+            me.getView().validate();
+        },
+
+        listen: {
+            component: {
+                '*': {
+                    change: function () {
+                        let me = this;
+                        me.getView().checkChange();
+                    },
+                },
+            },
+        },
+    },
+
+    getValue: function () {
+        let me = this;
+        let ctrl = me.getController();
+        if (ctrl.lookupReference('modeDefault').getValue()) {
+            return '';
+        } else if (ctrl.lookupReference('modeHost').getValue()) {
+            return 'host';
+        } else if (ctrl.lookupReference('modeCustom').getValue()) {
+            return ctrl.lookupReference('customNum').getValue();
+        }
+        return ''; // shouldn't happen
+    },
+
+    setValue: function (value) {
+        let me = this;
+        let ctrl = me.getController();
+        let modeField;
+
+        if (!value) {
+            modeField = ctrl.lookupReference('modeDefault');
+        } else if (value === 'host') {
+            modeField = ctrl.lookupReference('modeHost');
+        } else {
+            let customNum = ctrl.lookupReference('customNum');
+            customNum.setValue(value);
+            modeField = ctrl.lookupReference('modeCustom');
+        }
+
+        modeField.setValue(true);
+        me.checkChange();
+
+        return value;
+    },
+
+    getErrors: function () {
+        let me = this;
+        let ctrl = me.getController();
+        if (ctrl.lookupReference('modeCustom').getValue()) {
+            return ctrl.lookupReference('customNum').getErrors();
+        }
+        return [];
+    },
+
+    isValid: function () {
+        let me = this;
+        let ctrl = me.getController();
+        if (ctrl.lookupReference('modeCustom').getValue()) {
+            return ctrl.lookupReference('customNum').isValid();
+        }
+        return true;
+    },
+
+    items: [
+        {
+            xtype: 'radiofield',
+            boxLabel: gettext('Default'),
+            inputValue: 'default',
+            checked: true,
+            reference: 'modeDefault',
+            listeners: {
+                change: 'onRadioChange',
+            },
+            isFormField: false,
+        },
+        {
+            xtype: 'radiofield',
+            boxLabel: gettext('Host'),
+            inputValue: 'host',
+            reference: 'modeHost',
+            listeners: {
+                change: 'onRadioChange',
+            },
+            isFormField: false,
+        },
+        {
+            xtype: 'fieldcontainer',
+            layout: 'hbox',
+            items: [
+                {
+                    xtype: 'radiofield',
+                    boxLabel: gettext('Custom'),
+                    inputValue: 'custom',
+                    listeners: {
+                        change: 'onRadioChange',
+                    },
+                    reference: 'modeCustom',
+                    isFormField: false,
+                },
+                {
+                    xtype: 'numberfield',
+                    width: 60,
+                    margin: '0 0 0 10px',
+                    minValue: 8,
+                    maxValue: 64,
+                    reference: 'customNum',
+                    allowBlank: false,
+                    isFormField: false,
+                    disabled: true,
+                },
+            ],
+        },
+    ],
+});
-- 
2.47.3



Reply via email to