Add GUI wizard to import whole VMs and a window to import single disks in
Hardware View.

Signed-off-by: Dominic Jäger <d.jae...@proxmox.com>

---
v6->v7:
- Update to API changes
- Add helpers to Utils
- Whitespace & line breaks according to style guide
- Making conditional branches in HDEdit easier to read

 PVE/API2/Nodes.pm                       |   7 +
 www/manager6/Makefile                   |   2 +
 www/manager6/Utils.js                   |  12 +
 www/manager6/Workspace.js               |  15 ++
 www/manager6/form/ControllerSelector.js |  15 ++
 www/manager6/node/CmdMenu.js            |  13 +
 www/manager6/qemu/HDEdit.js             | 169 +++++++++++-
 www/manager6/qemu/HardwareView.js       |  25 ++
 www/manager6/qemu/ImportWizard.js       | 332 ++++++++++++++++++++++++
 www/manager6/qemu/MultiHDEdit.js        | 277 ++++++++++++++++++++
 www/manager6/window/Wizard.js           |   2 +
 11 files changed, 856 insertions(+), 13 deletions(-)
 create mode 100644 www/manager6/qemu/ImportWizard.js
 create mode 100644 www/manager6/qemu/MultiHDEdit.js

diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm
index ba6621c6..1cee6cb5 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -48,6 +48,7 @@ use PVE::API2::LXC;
 use PVE::API2::Network;
 use PVE::API2::NodeConfig;
 use PVE::API2::Qemu::CPU;
+use PVE::API2::Qemu::OVF;
 use PVE::API2::Qemu;
 use PVE::API2::Replication;
 use PVE::API2::Services;
@@ -76,6 +77,11 @@ __PACKAGE__->register_method ({
     path => 'cpu',
 });
 
+__PACKAGE__->register_method ({
+    subclass => "PVE::API2::Qemu::OVF",
+    path => 'readovf',
+});
+
 __PACKAGE__->register_method ({
     subclass => "PVE::API2::LXC",
     path => 'lxc',
@@ -2183,6 +2189,7 @@ __PACKAGE__->register_method ({
        return undef;
     }});
 
+
 # bash completion helper
 
 sub complete_templet_repo {
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index a2f7be6d..753cd1c0 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -196,8 +196,10 @@ JSSRC=                                                     
\
        qemu/CmdMenu.js                                 \
        qemu/Config.js                                  \
        qemu/CreateWizard.js                            \
+       qemu/ImportWizard.js                            \
        qemu/DisplayEdit.js                             \
        qemu/HDEdit.js                                  \
+       qemu/MultiHDEdit.js                                     \
        qemu/HDEfi.js                                   \
        qemu/HDMove.js                                  \
        qemu/HDResize.js                                \
diff --git a/www/manager6/Utils.js b/www/manager6/Utils.js
index f502950f..dbfd65ce 100644
--- a/www/manager6/Utils.js
+++ b/www/manager6/Utils.js
@@ -1708,6 +1708,16 @@ Ext.define('PVE.Utils', {
        });
     },
 
+    // collection ... collection of strings of a subset of the descendants of 
container
+    // visible ... true to show and enable, false to hide and disable
+    setDescendantsVisible: function(container, collection, visible = 1) {
+       const hide = (element, value) => {
+           element.setHidden(value);
+           element.setDisabled(value);
+       };
+       collection.map(e => container.down(e)).forEach(e => hide(e, !visible));
+    },
+
     cpu_vendor_map: {
        'default': 'QEMU',
        'AuthenticAMD': 'AMD',
@@ -1787,6 +1797,8 @@ Ext.define('PVE.Utils', {
            hastop: ['HA', gettext('Stop')],
            imgcopy: ['', gettext('Copy data')],
            imgdel: ['', gettext('Erase data')],
+           importdisk: ['VM', gettext('Import disk')],
+           importvm: ['VM', gettext('Import VM')],
            lvmcreate: [gettext('LVM Storage'), gettext('Create')],
            lvmthincreate: [gettext('LVM-Thin Storage'), gettext('Create')],
            migrateall: ['', gettext('Migrate all VMs and Containers')],
diff --git a/www/manager6/Workspace.js b/www/manager6/Workspace.js
index 0c1b9e0c..631739a0 100644
--- a/www/manager6/Workspace.js
+++ b/www/manager6/Workspace.js
@@ -280,11 +280,25 @@ Ext.define('PVE.StdWorkspace', {
            },
        });
 
+       var importVM = Ext.createWidget('button', {
+           pack: 'end',
+           margin: '3 5 0 0',
+           baseCls: 'x-btn',
+           iconCls: 'fa fa-desktop',
+           text: gettext("Import VM"),
+           hidden: Proxmox.UserName !== 'root@pam',
+           handler: function() {
+               var wiz = Ext.create('PVE.qemu.ImportWizard', {});
+               wiz.show();
+           },
+       });
+
        sprovider.on('statechange', function(sp, key, value) {
            if (key === 'GuiCap' && value) {
                caps = value;
                createVM.setDisabled(!caps.vms['VM.Allocate']);
                createCT.setDisabled(!caps.vms['VM.Allocate']);
+               importVM.setDisabled(!caps.vms['VM.Allocate']);
            }
        });
 
@@ -332,6 +346,7 @@ Ext.define('PVE.StdWorkspace', {
                        },
                        createVM,
                        createCT,
+                       importVM,
                        {
                            pack: 'end',
                            margin: '0 5 0 0',
diff --git a/www/manager6/form/ControllerSelector.js 
b/www/manager6/form/ControllerSelector.js
index 23c61159..8e9aee98 100644
--- a/www/manager6/form/ControllerSelector.js
+++ b/www/manager6/form/ControllerSelector.js
@@ -68,6 +68,21 @@ clist_loop:
        deviceid.validate();
     },
 
+    getValues: function() {
+       return this.query('field').map(x => x.getValue());
+    },
+
+    getValuesAsString: function() {
+       return this.getValues().join('');
+    },
+
+    setValue: function(value) {
+       let regex = /([a-z]+)(\d+)/;
+       let [_, controller, deviceid] = regex.exec(value);
+       this.down('field[name=controller]').setValue(controller);
+       this.down('field[name=deviceid]').setValue(deviceid);
+    },
+
     initComponent: function() {
        var me = this;
 
diff --git a/www/manager6/node/CmdMenu.js b/www/manager6/node/CmdMenu.js
index b650bfa0..407cf2d0 100644
--- a/www/manager6/node/CmdMenu.js
+++ b/www/manager6/node/CmdMenu.js
@@ -29,6 +29,19 @@ Ext.define('PVE.node.CmdMenu', {
                wiz.show();
            },
        },
+       {
+           text: gettext("Import VM"),
+           hidden: Proxmox.UserName !== 'root@pam',
+           itemId: 'importvm',
+           iconCls: 'fa fa-cube',
+           handler: function() {
+               var me = this.up('menu');
+               var wiz = Ext.create('PVE.qemu.ImportWizard', {
+                   nodename: me.nodename,
+               });
+               wiz.show();
+           },
+       },
        { xtype: 'menuseparator' },
        {
            text: gettext('Bulk Start'),
diff --git a/www/manager6/qemu/HDEdit.js b/www/manager6/qemu/HDEdit.js
index e22111bf..a2f6c95a 100644
--- a/www/manager6/qemu/HDEdit.js
+++ b/www/manager6/qemu/HDEdit.js
@@ -58,6 +58,21 @@ Ext.define('PVE.qemu.HDInputPanel', {
        },
     },
 
+    isImport: function() {
+       return this.isImportVM || this.isImportDisk;
+    },
+
+    /*
+    All radiofields (esp. sourceRadioPath and sourceRadioStorage) have the
+    same scope for name. But we need a different scope for each HDInputPanel in
+    a MultiHDInputPanel to get the selection for each HDInputPanel => Make
+    names so that those within one HDInputPanel are equal, but different from 
other
+    HDInputPanels
+    */
+    getSourceTypeID() {
+       return 'sourceType_' + this.id;
+    },
+
     onGetValues: function(values) {
        var me = this;
 
@@ -70,6 +85,8 @@ Ext.define('PVE.qemu.HDInputPanel', {
        } else if (me.isCreate) {
            if (values.hdimage) {
                me.drive.file = values.hdimage;
+           } else if (me.isImport()) {
+               me.drive.file = `${values.hdstorage}:0`;
            } else {
                me.drive.file = values.hdstorage + ":" + values.disksize;
            }
@@ -83,15 +100,33 @@ Ext.define('PVE.qemu.HDInputPanel', {
        PVE.Utils.propertyStringSet(me.drive, values.iothread, 'iothread', 
'on');
        PVE.Utils.propertyStringSet(me.drive, values.cache, 'cache');
 
-        var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
-        Ext.Array.each(names, function(name) {
-            var burst_name = name + '_max';
+       var names = ['mbps_rd', 'mbps_wr', 'iops_rd', 'iops_wr'];
+       Ext.Array.each(names, function(name) {
+           var burst_name = name + '_max';
            PVE.Utils.propertyStringSet(me.drive, values[name], name);
            PVE.Utils.propertyStringSet(me.drive, values[burst_name], 
burst_name);
-        });
+       });
+
+       const getSourceImageLocation = function() {
+           const type = values[me.getSourceTypeID()];
+           return type === 'storage' ? values.sourceVolid : values.sourcePath;
+       };
 
+       if (me.isImportVM) {
+           params.diskimage = `${confid}=${getSourceImageLocation()}`;
+       }
+
+       const options = PVE.Parser.printQemuDrive(me.drive);
 
-       params[confid] = PVE.Parser.printQemuDrive(me.drive);
+       if (me.isImportDisk) {
+           params.device = confid;
+           params.device_options = options;
+           params.source = getSourceImageLocation();
+           params.device = values.controller + values.deviceid;
+           params.storage = values.hdstorage;
+       } else {
+           params[confid] = options;
+       }
 
        return params;
     },
@@ -149,6 +184,10 @@ Ext.define('PVE.qemu.HDInputPanel', {
        me.setValues(values);
     },
 
+    getDevice: function() {
+           return this.bussel.getValuesAsString();
+    },
+
     setNodename: function(nodename) {
        var me = this;
        me.down('#hdstorage').setNodename(nodename);
@@ -169,10 +208,15 @@ Ext.define('PVE.qemu.HDInputPanel', {
        me.advancedColumn2 = [];
 
        if (!me.confid || me.unused) {
+           let controllerColumn = me.isImport() ? me.column2 : me.column1;
            me.bussel = Ext.create('PVE.form.ControllerSelector', {
+               itemId: 'bussel',
                vmconfig: me.insideWizard ? { ide2: 'cdrom' } : {},
            });
-           me.column1.push(me.bussel);
+           if (me.isImport()) {
+               me.bussel.fieldLabel = 'Target Device';
+           }
+           controllerColumn.push(me.bussel);
 
            me.scsiController = Ext.create('Ext.form.field.Display', {
                fieldLabel: gettext('SCSI Controller'),
@@ -184,7 +228,7 @@ Ext.define('PVE.qemu.HDInputPanel', {
                submitValue: false,
                hidden: true,
            });
-           me.column1.push(me.scsiController);
+           controllerColumn.push(me.scsiController);
        }
 
        if (me.unused) {
@@ -199,14 +243,21 @@ Ext.define('PVE.qemu.HDInputPanel', {
                allowBlank: false,
            });
            me.column1.push(me.unusedDisks);
-       } else if (me.isCreate) {
-           me.column1.push({
+       } else if (me.isCreate || me.isImport()) {
+           let selector = {
                xtype: 'pveDiskStorageSelector',
                storageContent: 'images',
                name: 'disk',
                nodename: me.nodename,
-               autoSelect: me.insideWizard,
-           });
+               hideSize: me.isImport(),
+               autoSelect: me.insideWizard || me.isImport(),
+           };
+           if (me.isImport()) {
+               selector.storageLabel = gettext('Target storage');
+               me.column2.push(selector);
+           } else {
+               me.column1.push(selector);
+           }
        } else {
            me.column1.push({
                xtype: 'textfield',
@@ -217,6 +268,12 @@ Ext.define('PVE.qemu.HDInputPanel', {
            });
        }
 
+       if (me.isImport()) {
+           me.column2.push({
+               xtype: 'box',
+               autoEl: { tag: 'hr' },
+           });
+       }
        me.column2.push(
            {
                xtype: 'CacheTypeSelector',
@@ -231,6 +288,84 @@ Ext.define('PVE.qemu.HDInputPanel', {
                name: 'discard',
            },
        );
+       if (me.isImport()) {
+           me.column1.unshift(
+               {
+                   xtype: 'radiofield',
+                   itemId: 'sourceRadioStorage',
+                   name: me.getSourceTypeID(),
+                   inputValue: 'storage',
+                   boxLabel: gettext('Use a storage as source'),
+                   hidden: Proxmox.UserName !== 'root@pam',
+                   checked: true,
+                   listeners: {
+                       change: (_, newValue) => {
+                           const selectors = [
+                               '#sourceStorageSelector',
+                               '#sourceFileSelector',
+                           ];
+                           PVE.Utils.setDescendantsVisible(me, selectors, 
newValue);
+                       },
+                   },
+               }, {
+                   xtype: 'pveStorageSelector',
+                   itemId: 'sourceStorageSelector',
+                   name: 'inputImageStorage',
+                   nodename: me.nodename,
+                   fieldLabel: gettext('Source Storage'),
+                   storageContent: 'images',
+                   autoSelect: me.insideWizard,
+                   hidden: true,
+                   disabled: true,
+                   listeners: {
+                       change: function(_, selectedStorage) {
+                           
me.down('#sourceFileSelector').setStorage(selectedStorage);
+                       },
+                   },
+               }, {
+                   xtype: 'pveFileSelector',
+                   itemId: 'sourceFileSelector',
+                   name: 'sourceVolid',
+                   nodename: me.nodename,
+                   storageContent: 'images',
+                   hidden: true,
+                   disabled: true,
+                   fieldLabel: gettext('Source Image'),
+                   autoEl: {
+                       tag: 'div',
+                       'data-qtip': gettext("Place your source images into a 
new folder <storageRoot>/images/<newVMID>, for example /var/lib/vz/images/999"),
+                   },
+               }, {
+                   xtype: 'radiofield',
+                   itemId: 'sourceRadioPath',
+                   name: me.getSourceTypeID(),
+                   inputValue: 'path',
+                   boxLabel: gettext('Use an absolute path as source'),
+                   hidden: Proxmox.UserName !== 'root@pam',
+                   listeners: {
+                       change: (_, newValue) => {
+                           PVE.Utils.setDescendantsVisible(me, 
['#sourcePathTextfield'], newValue);
+                       },
+                   },
+               }, {
+                   xtype: 'textfield',
+                   itemId: 'sourcePathTextfield',
+                   fieldLabel: gettext('Source Path'),
+                   name: 'sourcePath',
+                   autoEl: {
+                       tag: 'div',
+                       'data-qtip': gettext('Absolute path to the source disk 
image, for example: /home/user/somedisk.qcow2'),
+                   },
+                   hidden: true,
+                   disabled: true,
+                   validator: function(insertedText) {
+                       return insertedText.startsWith('/') ||
+                           insertedText.startsWith('http') ||
+                           gettext('Must be an absolute path or URL');
+                   },
+               },
+           );
+       }
 
        me.advancedColumn1.push(
            {
@@ -373,13 +508,18 @@ Ext.define('PVE.qemu.HDEdit', {
            nodename: nodename,
            unused: unused,
            isCreate: me.isCreate,
+           isImportVM: me.isImportVM,
+           isImportDisk: me.isImportDisk,
        });
 
-       var subject;
        if (unused) {
            me.subject = gettext('Unused Disk');
+       } else if (me.isImportDisk) {
+           me.subject = gettext('Import Disk');
+           me.submitText = 'Import';
+           me.backgroundDelay = undefined;
        } else if (me.isCreate) {
-            me.subject = gettext('Hard Disk');
+           me.subject = gettext('Hard Disk');
        } else {
            me.subject = gettext('Hard Disk') + ' (' + me.confid + ')';
        }
@@ -404,6 +544,9 @@ Ext.define('PVE.qemu.HDEdit', {
                    ipanel.setDrive(drive);
                    me.isValid(); // trigger validation
                }
+               if (me.isImportDisk) {
+                   me.url = me.url.replace(/\/config$/, "/importdisk");
+               }
            },
        });
     },
diff --git a/www/manager6/qemu/HardwareView.js 
b/www/manager6/qemu/HardwareView.js
index 98352e3f..4fbf0e5e 100644
--- a/www/manager6/qemu/HardwareView.js
+++ b/www/manager6/qemu/HardwareView.js
@@ -431,6 +431,30 @@ Ext.define('PVE.qemu.HardwareView', {
            handler: run_move,
        });
 
+       var import_btn = new Proxmox.button.Button({
+           text: gettext('Import disk'),
+           hidden: Proxmox.UserName !== 'root@pam',
+           handler: function() {
+               var win = Ext.create('PVE.qemu.HDEdit', {
+                   method: 'POST',
+                   url: `/api2/extjs/${baseurl}`,
+                   pveSelNode: me.pveSelNode,
+                   isImportDisk: true,
+                   listeners: {
+                       add: function(_, component) {
+                           const selectors = [
+                               '#sourceStorageSelector',
+                               '#sourceFileSelector',
+                           ];
+                           PVE.Utils.setDescendantsVisible(component, 
selectors);
+                       },
+                   },
+               });
+               win.on('destroy', me.reload, me);
+               win.show();
+           },
+       });
+
        var remove_btn = new Proxmox.button.Button({
            text: gettext('Remove'),
            defaultText: gettext('Remove'),
@@ -759,6 +783,7 @@ Ext.define('PVE.qemu.HardwareView', {
                edit_btn,
                resize_btn,
                move_btn,
+               import_btn,
                revert_btn,
            ],
            rows: rows,
diff --git a/www/manager6/qemu/ImportWizard.js 
b/www/manager6/qemu/ImportWizard.js
new file mode 100644
index 00000000..0066adc4
--- /dev/null
+++ b/www/manager6/qemu/ImportWizard.js
@@ -0,0 +1,332 @@
+/*jslint confusion: true*/
+Ext.define('PVE.qemu.ImportWizard', {
+    extend: 'PVE.window.Wizard',
+    alias: 'widget.pveQemuImportWizard',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    viewModel: {
+       data: {
+           nodename: '',
+           current: {
+               scsihw: '',
+           },
+       },
+    },
+
+    cbindData: {
+       nodename: undefined,
+    },
+
+    subject: gettext('Import Virtual Machine'),
+
+    isImportVM: true,
+
+    addDiskFunction: function() {
+       let me = this;
+       let wizard;
+       if (me.xtype === 'button') {
+               wizard = me.up('window');
+       } else if (me.xtype === 'pveQemuImportWizard') {
+               wizard = me;
+       }
+       let multihd = wizard.down('pveQemuMultiHDInputPanel');
+       multihd.addDiskFunction();
+    },
+
+    items: [
+       {
+           xtype: 'inputpanel',
+           title: gettext('Import'),
+           itemId: 'importInputpanel',
+           column1: [
+               {
+                   xtype: 'pveNodeSelector',
+                   name: 'nodename',
+                   cbind: {
+                       selectCurNode: '{!nodename}',
+                       preferredValue: '{nodename}',
+                   },
+                   bind: {
+                       value: '{nodename}',
+                   },
+                   fieldLabel: gettext('Node'),
+                   allowBlank: false,
+                   onlineValidator: true,
+               },
+               {
+                   xtype: 'pveGuestIDSelector',
+                   name: 'vmid',
+                   guestType: 'qemu',
+                   value: '',
+                   loadNextFreeID: true,
+                   validateExists: false,
+               },
+           ],
+           column2: [
+               {
+                   xtype: 'label',
+                   itemId: 'successTextfield',
+                   hidden: true,
+                   html: gettext('Manifest successfully uploaded'),
+                   margin: '0 0 0 10',
+               },
+               {
+                   xtype: 'textfield',
+                   itemId: 'server_ovf_manifest',
+                   name: 'ovf_textfield',
+                   emptyText: '/mnt/nfs/exported.ovf',
+                   fieldLabel: 'Absolute path to .ovf manifest on your PVE 
host',
+                   listeners: {
+                       validitychange: function(_, isValid) {
+                           let button = 
Ext.ComponentQuery.query('#load_remote_manifest_button').pop();
+                           button.setDisabled(!isValid);
+                       },
+                   },
+                   validator: function(value) {
+                       return (value && value.startsWith('/')) || 
gettext("Must start with /");
+                   },
+               },
+               {
+                   xtype: 'proxmoxButton',
+                   itemId: 'load_remote_manifest_button',
+                   text: gettext('Load remote manifest'),
+                   disabled: true,
+                   handler: function() {
+                       let inputpanel = this.up('#importInputpanel');
+                       let nodename = 
inputpanel.down('pveNodeSelector').getValue();
+                        // independent of onGetValues(), so that value of
+                        // ovf_textfield can be removed for submit
+                       let ovf_textfield_value = 
inputpanel.down('textfield[name=ovf_textfield]').getValue();
+                       let wizard = this.up('window');
+                       Proxmox.Utils.API2Request({
+                           url: '/nodes/' + nodename + '/readovf',
+                           method: 'GET',
+                           params: {
+                               manifest: ovf_textfield_value,
+                           },
+                           success: function(response) {
+                               let ovfdata = response.result.data;
+                               
wizard.down('#vmNameTextfield').setValue(ovfdata.name);
+                               
wizard.down('#cpupanel').getViewModel().set('coreCount', ovfdata.cores);
+                               
wizard.down('#memorypanel').down('pveMemoryField').setValue(ovfdata.memory);
+                               delete ovfdata.name;
+                               delete ovfdata.cores;
+                               delete ovfdata.memory;
+                               delete ovfdata.digest;
+                               let devices = Object.keys(ovfdata); // e.g. 
ide0, sata2
+                               let multihd = 
wizard.down('pveQemuMultiHDInputPanel');
+                               if (devices.length > 0) {
+                                   multihd.removeAllDisks();
+                               }
+                               for (var device of devices) {
+                                   multihd.addDiskFunction(device, 
ovfdata[device]);
+                               }
+                           },
+                           failure: function(response, opts) {
+                               console.warn("Failure of load manifest button");
+                               console.warn(response);
+                           },
+                       });
+                   },
+               },
+           ],
+           onGetValues: function(values) {
+               delete values.server_ovf_manifest;
+               delete values.ovf_textfield;
+               return values;
+           },
+       },
+       {
+           xtype: 'inputpanel',
+           title: gettext('General'),
+           onlineHelp: 'qm_general_settings',
+           column1: [
+               {
+                   xtype: 'textfield',
+                   name: 'name',
+                   itemId: 'vmNameTextfield',
+                   vtype: 'DnsName',
+                   value: '',
+                   fieldLabel: gettext('Name'),
+                   allowBlank: true,
+               },
+           ],
+           column2: [
+               {
+                   xtype: 'pvePoolSelector',
+                   fieldLabel: gettext('Resource Pool'),
+                   name: 'pool',
+                   value: '',
+                   allowBlank: true,
+               },
+           ],
+           advancedColumn1: [
+               {
+                   xtype: 'proxmoxcheckbox',
+                   name: 'onboot',
+                   uncheckedValue: 0,
+                   defaultValue: 0,
+                   deleteDefaultValue: true,
+                   fieldLabel: gettext('Start at boot'),
+               },
+           ],
+           advancedColumn2: [
+               {
+                   xtype: 'textfield',
+                   name: 'order',
+                   defaultValue: '',
+                   emptyText: 'any',
+                   labelWidth: 120,
+                   fieldLabel: gettext('Start/Shutdown order'),
+               },
+               {
+                   xtype: 'textfield',
+                   name: 'up',
+                   defaultValue: '',
+                   emptyText: 'default',
+                   labelWidth: 120,
+                   fieldLabel: gettext('Startup delay'),
+               },
+               {
+                   xtype: 'textfield',
+                   name: 'down',
+                   defaultValue: '',
+                   emptyText: 'default',
+                   labelWidth: 120,
+                   fieldLabel: gettext('Shutdown timeout'),
+               },
+           ],
+           onGetValues: function(values) {
+               ['name', 'pool', 'onboot', 'agent'].forEach(function(field) {
+                   if (!values[field]) {
+                       delete values[field];
+                   }
+               });
+
+               var res = PVE.Parser.printStartup({
+                   order: values.order,
+                   up: values.up,
+                   down: values.down,
+               });
+
+               if (res) {
+                   values.startup = res;
+               }
+
+               delete values.order;
+               delete values.up;
+               delete values.down;
+
+               return values;
+           },
+       },
+       {
+           xtype: 'pveQemuSystemPanel',
+           title: gettext('System'),
+           isCreate: true,
+           insideWizard: true,
+       },
+       {
+           xtype: 'pveQemuMultiHDInputPanel',
+           title: gettext('Hard Disk'),
+           bind: {
+               nodename: '{nodename}',
+           },
+           isCreate: true,
+           insideWizard: true,
+       },
+       {
+           itemId: 'cpupanel',
+           xtype: 'pveQemuProcessorPanel',
+           insideWizard: true,
+           title: gettext('CPU'),
+       },
+       {
+           itemId: 'memorypanel',
+           xtype: 'pveQemuMemoryPanel',
+           insideWizard: true,
+           title: gettext('Memory'),
+       },
+       {
+           xtype: 'pveQemuNetworkInputPanel',
+           bind: {
+               nodename: '{nodename}',
+           },
+           title: gettext('Network'),
+           insideWizard: true,
+       },
+       {
+           title: gettext('Confirm'),
+           layout: 'fit',
+           items: [
+               {
+                   xtype: 'grid',
+                   store: {
+                       model: 'KeyValue',
+                       sorters: [{
+                           property: 'key',
+                           direction: 'ASC',
+                       }],
+                   },
+                   columns: [
+                       { header: 'Key', width: 150, dataIndex: 'key' },
+                       { header: 'Value', flex: 1, dataIndex: 'value' },
+                   ],
+               },
+           ],
+           dockedItems: [
+               {
+                   xtype: 'proxmoxcheckbox',
+                   name: 'start',
+                   dock: 'bottom',
+                   margin: '5 0 0 0',
+                   boxLabel: gettext('Start after created'),
+               },
+           ],
+           listeners: {
+               show: function(panel) {
+                   var kv = this.up('window').getValues();
+                   var data = [];
+                   Ext.Object.each(kv, function(key, value) {
+                       if (key === 'delete') { // ignore
+                           return;
+                       }
+                       data.push({ key: key, value: value });
+                   });
+
+                   var summarystore = panel.down('grid').getStore();
+                   summarystore.suspendEvents();
+                   summarystore.removeAll();
+                   summarystore.add(data);
+                   summarystore.sort();
+                   summarystore.resumeEvents();
+                   summarystore.fireEvent('refresh');
+               },
+           },
+           onSubmit: function() {
+               var wizard = this.up('window');
+               var params = wizard.getValues();
+
+               var nodename = params.nodename;
+               delete params.nodename;
+               delete params.delete;
+               if (Array.isArray(params.diskimages)) {
+                       params.diskimages = params.diskimages.join(',');
+               }
+
+               Proxmox.Utils.API2Request({
+                   url: `/nodes/${nodename}/qemu/${params.vmid}/importvm`,
+                   waitMsgTarget: wizard,
+                   method: 'POST',
+                   params: params,
+                   success: function() {
+                       wizard.close();
+                   },
+                   failure: function(response) {
+                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+                   },
+               });
+           },
+       },
+    ],
+});
diff --git a/www/manager6/qemu/MultiHDEdit.js b/www/manager6/qemu/MultiHDEdit.js
new file mode 100644
index 00000000..641a802f
--- /dev/null
+++ b/www/manager6/qemu/MultiHDEdit.js
@@ -0,0 +1,277 @@
+Ext.define('PVE.qemu.MultiHDInputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveQemuMultiHDInputPanel',
+
+    insideWizard: false,
+
+    hiddenDisks: [],
+
+    leftColumnRatio: 0.25,
+
+    column1: [
+       {
+           // Adding to the HDInputPanelContainer below automatically adds
+           // items to this store
+           xtype: 'gridpanel',
+           scrollable: true,
+           store: {
+               xtype: 'store',
+               storeId: 'importwizard_diskstorage',
+               // Use the panel as id
+               // Panels have are objects and therefore unique
+               // E.g. while adding new panels 'device' is ambiguous
+               fields: ['device', 'panel'],
+               removeByPanel: function(panel) {
+                   let recordIndex = this.findBy(record => record.data.panel 
=== panel);
+                   this.removeAt(recordIndex);
+                   return recordIndex;
+               },
+           },
+           columns: [
+               {
+                   text: gettext('Target device'),
+                   dataIndex: 'device',
+                   flex: 1,
+                   resizable: false,
+               },
+           ],
+           listeners: {
+               select: function(_, record) {
+                   this.up('pveQemuMultiHDInputPanel')
+                       .down('#HDInputPanelContainer')
+                       .setActiveItem(record.data.panel);
+               },
+           },
+           anchor: '100% 90%',
+       }, {
+           xtype: 'container',
+           layout: 'hbox',
+           center: true,
+           defaults: {
+               margin: '5',
+               xtype: 'button',
+           },
+           items: [
+               {
+                   iconCls: 'fa fa-plus-circle',
+                   itemId: 'addDisk',
+                   handler: function(button) {
+                       button.up('pveQemuMultiHDInputPanel').addDiskFunction();
+                   },
+               }, {
+                   iconCls: 'fa fa-trash-o',
+                   itemId: 'removeDisk',
+                   handler: function(button) {
+                       
button.up('pveQemuMultiHDInputPanel').removeCurrentDisk();
+                   },
+               },
+           ],
+       },
+    ],
+    column2: [
+       {
+           itemId: 'HDInputPanelContainer',
+           xtype: 'container',
+           layout: 'card',
+           items: [],
+           listeners: {
+               beforeRender: function() {
+                   // Initial disk if none have been added by manifest yet
+                   if (this.items.items.length === 0) {
+                       this.addDiskFunction();
+                   }
+               },
+               add: function(container, newPanel, index) {
+                   let store = Ext.getStore('importwizard_diskstorage');
+                   store.add({ device: newPanel.getDevice(), panel: newPanel 
});
+                   container.setActiveItem(newPanel);
+               },
+               remove: function(HDInputPanelContainer, HDInputPanel, eOpts) {
+                   let store = Ext.getStore('importwizard_diskstorage');
+                   let indexOfRemoved = store.removeByPanel(HDInputPanel);
+                   if (HDInputPanelContainer.items.getCount() > 0) {
+                       HDInputPanelContainer.setActiveItem(indexOfRemoved - 1);
+                   }
+               },
+           },
+           defaultItem: {
+               xtype: 'pveQemuHDInputPanel',
+               bind: {
+                   nodename: '{nodename}',
+               },
+               isCreate: true,
+               isImportVM: true,
+               returnSingleKey: true,
+               insideWizard: true,
+               setNodename: function(nodename) {
+                   this.down('#hdstorage').setNodename(nodename);
+                   this.down('#hdimage').setStorage(undefined, nodename);
+                   this.down('#sourceStorageSelector').setNodename(nodename);
+                   this.down('#sourceFileSelector').setNodename(nodename);
+               },
+               listeners: {
+                   // newHDInputPanel ... the defaultItem that has just been
+                   //   cloned and added into HDInputPnaleContainer parameter
+                   // HDInputPanelContainer ... the container from column2
+                   //   where all the new panels go into
+                   added: function(newHDInputPanel, HDInputPanelContainer, 
pos) {
+                       // The listeners cannot be added earlier, because its 
fields don't exist earlier
+                       Ext.Array.each(this.down('pveControllerSelector')
+                           .query('field'), function(field) {
+                               field.on('change', function() {
+                                   // Note that one setValues in a controller
+                                   // selector makes one setValue in each of
+                                   // the two fields, so this listener fires
+                                   // two times in a row so to say e.g.
+                                   // changing controller selector from ide0 to
+                                   // sata1 makes ide0->sata0 and then
+                                   // sata0->sata1
+                                   let store = 
Ext.getStore('importwizard_diskstorage');
+                                   let controllerSelector = 
field.up('pveQemuHDInputPanel')
+                                       .down('pveControllerSelector');
+                                   /*
+                                    * controller+device (ide0) might be
+                                    * ambiguous during creation => find by
+                                    * panel object instead
+                                    *
+                                    * There is no function that takes a
+                                    * function and returns the model directly
+                                    * => index & getAt
+                                    */
+                                   let recordIndex = store.findBy(record =>
+                                       record.data.panel === 
field.up('pveQemuHDInputPanel'),
+                                   );
+                                   let newControllerAndId = 
controllerSelector.getValuesAsString();
+                                   store.getAt(recordIndex).set('device', 
newControllerAndId);
+                               });
+                           },
+                       );
+                       let wizard = this.up('pveQemuImportWizard');
+                       Ext.Array.each(this.query('field'), function(field) {
+                           field.on('change', wizard.validcheck);
+                           field.on('validitychange', wizard.validcheck);
+                       });
+                   },
+               },
+               validator: function() {
+                   var valid = true;
+                   var fields = this.query('field, fieldcontainer');
+                   Ext.Array.each(fields, function(field) {
+                       // Note: not all fielcontainer have isValid()
+                       if (Ext.isFunction(field.isValid) && !field.isValid()) {
+                           valid = false;
+                       }
+                   });
+                   return valid;
+               },
+           },
+
+           // device ... device that the new disk should be assigned to, e.g.
+           //   ide0, sata2
+           // path ... if this is set to x then the disk will
+           //   backed/imported from the path x, that is, the textfield will
+           //   contain the value x
+           addDiskFunction(device, path) {
+               // creating directly removes binding => no storage found?
+               let item = Ext.clone(this.defaultItem);
+               let added = this.add(item);
+               // At this point the 'added' listener has fired and the fields
+               // in the variable added have the change listeners that update
+               // the store Therefore we can now set values only on the field
+               // and they will be updated in the store
+               if (path) {
+                   added.down('#sourceRadioPath').setValue(true);
+                   added.down('#sourcePathTextfield').setValue(path);
+               } else {
+                   added.down('#sourceRadioStorage').setValue(true);
+                   added.down('#sourceStorageSelector').setHidden(false);
+                   added.down('#sourceFileSelector').setHidden(false);
+                   added.down('#sourceFileSelector').enable();
+                   added.down('#sourceStorageSelector').enable();
+               }
+
+               let sp = Ext.state.Manager.getProvider();
+               let advanced_checkbox = sp.get('proxmox-advanced-cb');
+               added.setAdvancedVisible(advanced_checkbox);
+
+               if (device) {
+                   // This happens after the 'add' and 'added' listeners of the
+                   // item/defaultItem clone/pveQemuHDInputPanel/added have 
fired
+                   added.down('pveControllerSelector').setValue(device);
+               }
+           },
+           removeCurrentDisk: function() {
+               let activePanel = this.getLayout().activeItem; // panel = disk
+               if (activePanel) {
+                   this.remove(activePanel);
+               }
+           },
+       },
+    ],
+
+    addDiskFunction: function(device, path) {
+       this.down('#HDInputPanelContainer').addDiskFunction(device, path);
+    },
+    removeCurrentDisk: function() {
+       this.down('#HDInputPanelContainer').removeCurrentDisk();
+    },
+    removeAllDisks: function() {
+       let container = this.down('#HDInputPanelContainer');
+       while (container.items.items.length > 0) {
+               container.removeCurrentDisk();
+       }
+    },
+
+    beforeRender: function() {
+       let leftColumnPanel = this.items.get(0).items.get(0);
+       leftColumnPanel.setFlex(this.leftColumnRatio);
+       // any other panel because this has no height yet
+       let panelHeight = this.up('tabpanel').items.items[0].getHeight();
+       leftColumnPanel.setHeight(panelHeight);
+    },
+
+    setNodename: function(nodename) {
+       this.nodename = nodename;
+    },
+
+    // Call with defined parameter or without
+    hasDuplicateDevices: function(values) {
+       if (!values) {
+           values = this.up('form').getValues();
+       }
+       if (!Array.isArray(values.controller)) {
+           return false;
+       }
+       for (let i = 0; i < values.controller.length - 1; i++) {
+           for (let j = i+1; j < values.controller.length; j++) {
+               if (values.controller[i] === values.controller[j]) {
+                   if (values.deviceid[i] === values.deviceid[j]) {
+                       return true;
+                   }
+               }
+           }
+       }
+       return false;
+    },
+
+    onGetValues: function(values) {
+       // Returning anything here would give wrong data in the form at the end
+       // of the wizrad Each HDInputPanel in this MultiHD panel already has a
+       // sufficient onGetValues() function for the form at the end of the
+       // wizard
+       if (this.hasDuplicateDevices(values)) {
+           Ext.Msg.alert(gettext('Error'), 'Equal target devices are 
forbidden. Make all unique!');
+       }
+    },
+
+    validator: function() {
+       let inputpanels = this.down('#HDInputPanelContainer').items.getRange();
+       if (inputpanels.some(panel => !panel.validator())) {
+           return false;
+       }
+       if (this.hasDuplicateDevices()) {
+           return false;
+       }
+       return true;
+    },
+});
diff --git a/www/manager6/window/Wizard.js b/www/manager6/window/Wizard.js
index 8b930bbd..a3e3b690 100644
--- a/www/manager6/window/Wizard.js
+++ b/www/manager6/window/Wizard.js
@@ -261,6 +261,8 @@ Ext.define('PVE.window.Wizard', {
            };
            field.on('change', validcheck);
            field.on('validitychange', validcheck);
+           // Make available for fields that get added later
+           me.validcheck = validcheck;
        });
     },
 });
-- 
2.20.1


_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel

Reply via email to