Signed-off-by: Dominic Jäger <d.jae...@proxmox.com>
---
Somehow selecting storages is not possible anymore. It was at some point.
You can add disks that are not in the OVF in the importwizard at the moment.

 PVE/API2/Nodes.pm                       |  48 +++
 www/manager6/Makefile                   |   2 +
 www/manager6/form/ControllerSelector.js |  26 +-
 www/manager6/qemu/HDEdit.js             | 219 +++++++++-----
 www/manager6/qemu/ImportWizard.js       | 379 ++++++++++++++++++++++++
 www/manager6/qemu/MultiHDEdit.js        | 267 +++++++++++++++++
 www/manager6/window/Wizard.js           | 153 +++++-----
 7 files changed, 940 insertions(+), 154 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 1b133352..b0e386f9 100644
--- a/PVE/API2/Nodes.pm
+++ b/PVE/API2/Nodes.pm
@@ -27,6 +27,7 @@ use PVE::HA::Env::PVE2;
 use PVE::HA::Config;
 use PVE::QemuConfig;
 use PVE::QemuServer;
+use PVE::QemuServer::OVF;
 use PVE::API2::Subscription;
 use PVE::API2::Services;
 use PVE::API2::Network;
@@ -224,6 +225,7 @@ __PACKAGE__->register_method ({
            { name => 'subscription' },
            { name => 'report' },
            { name => 'tasks' },
+           { name => 'readovf' },
            { name => 'rrd' }, # fixme: remove?
            { name => 'rrddata' },# fixme: remove?
            { name => 'replication' },
@@ -2137,6 +2139,52 @@ __PACKAGE__->register_method ({
        return undef;
     }});
 
+__PACKAGE__->register_method ({
+    name => 'readovf',
+    path => 'readovf',
+    method => 'GET',
+    protected => 1, # for worker upid file
+    proxyto => 'node',
+    description => "Read an .ovf manifest.",
+    parameters => {
+       additionalProperties => 0,
+       properties => {
+           node => get_standard_option('pve-node'),
+           manifest => {
+               description => ".ovf manifest",
+               type => 'string',
+           },
+       },
+    },
+    returns => {
+       description => "VM config according to .ovf manifest and digest of 
manifest",
+       type => "object",
+       properties => PVE::QemuServer::json_config_properties({
+           digest => {
+               type => 'string',
+               description => 'SHA1 digest of configuration file. This can be 
used to prevent concurrent modifications.',
+           },
+       }),
+    },
+    code => sub {
+       my ($param) = @_;
+
+       my $filename = '/tmp/readovflog';
+       open (my $fh, '>', $filename) or die "could not open file $filename";
+       my $parsed = PVE::QemuServer::OVF::parse_ovf($param->{manifest}, 1, 1);
+       my $result;
+       $result->{digest} = Digest::SHA::sha1_hex($param->{manifest});
+       $result->{cores} = $parsed->{qm}->{cores};
+       $result->{name} =  $parsed->{qm}->{name};
+       $result->{memory} = $parsed->{qm}->{memory};
+       
+       my $disks = $parsed->{disks};
+       foreach my $disk (@$disks) {
+           $result->{$disk->{disk_address}} = 
"importsource=".$disk->{backing_file};
+       }
+       return $result;
+}});
+
 # bash completion helper
 
 sub complete_templet_repo {
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 4fa8e1a3..bcd55fad 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -194,8 +194,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/form/ControllerSelector.js 
b/www/manager6/form/ControllerSelector.js
index 9fdae5d1..d9fbfe66 100644
--- a/www/manager6/form/ControllerSelector.js
+++ b/www/manager6/form/ControllerSelector.js
@@ -68,6 +68,22 @@ clist_loop:
        deviceid.validate();
     },
 
+    getValues: function() {
+       return this.query('field').map(x => x.getValue());
+    },
+
+    getValuesAsString: function() {
+       return this.getValues().join('');
+    },
+
+    setValue: function(value) {
+       console.assert(value);
+       let regex = /([a-z]+)(\d+)/;
+       [_, controller, deviceid] =  regex.exec(value);
+       this.query('field[name=controller]').pop().setValue(controller);
+       this.query('field[name=deviceid]').pop().setValue(deviceid);
+    },
+
     initComponent: function() {
        var me = this;
 
@@ -85,16 +101,6 @@ clist_loop:
                    noVirtIO: me.noVirtIO,
                    allowBlank: false,
                    flex: 2,
-                   listeners: {
-                       change: function(t, value) {
-                           if (!value) {
-                               return;
-                           }
-                           var field = me.down('field[name=deviceid]');
-                           
field.setMaxValue(PVE.Utils.diskControllerMaxIDs[value]);
-                           field.validate();
-                       }
-                   }
                },
                {
                    xtype: 'proxmoxintegerfield',
diff --git a/www/manager6/qemu/HDEdit.js b/www/manager6/qemu/HDEdit.js
index 5e0a3981..f8e811e1 100644
--- a/www/manager6/qemu/HDEdit.js
+++ b/www/manager6/qemu/HDEdit.js
@@ -8,6 +8,10 @@ Ext.define('PVE.qemu.HDInputPanel', {
 
     unused: false, // ADD usused disk imaged
 
+    showSourcePathTextfield: false, // to import a disk from an aritrary path
+
+    returnSingleKey: true, // {vmid}/importdisk expects multiple keys => false
+
     vmconfig: {}, // used to select usused disks
 
     viewModel: {},
@@ -58,6 +62,38 @@ Ext.define('PVE.qemu.HDInputPanel', {
        }
     },
 
+    /*
+    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 selectionf or each HDInputPanel => Make
+    names so that those in one HDInputPanel are equal but different from other
+    HDInputPanels
+    */
+    getSourceTypeIdentifier() {
+       console.assert(this.id);
+       return 'sourceType_' + this.id;
+    },
+
+    // values ... the values from onGetValues
+    getSourceValue: function(values) {
+       console.assert(values);
+       let result;
+       let type = values[this.getSourceTypeIdentifier()];
+       console.assert(type === 'storage' || type === 'path',
+           `type must be 'storage' or 'path' but is ${type}`);
+       if (type === 'storage') {
+           console.assert(values.sourceVolid,
+               "sourceVolid must be set when type is storage");
+           result = values.sourceVolid;
+       } else {
+           console.assert(values.sourcePath,
+               "sourcePath must be set when type is path");
+           result = values.sourcePath;
+       }
+       console.assert(result);
+       return result;
+    },
+
     onGetValues: function(values) {
        var me = this;
 
@@ -67,16 +103,18 @@ Ext.define('PVE.qemu.HDInputPanel', {
        if (me.unused) {
            me.drive.file = me.vmconfig[values.unusedId];
            confid = values.controller + values.deviceid;
-       } else if (me.isCreate && !me.isImport) {
+       } else if (me.isCreate) {
            // disk format & size should not be part of propertyString for 
import
            if (values.hdimage) {
                me.drive.file = values.hdimage;
+           } else if (me.isImport) {
+               me.drive.file = `${values.hdstorage}:0`; // so that API allows 
it
            } else {
                me.drive.file = values.hdstorage + ":" + values.disksize;
            }
            me.drive.format = values.diskformat;
        }
-
+       
        PVE.Utils.propertyStringSet(me.drive, !values.backup, 'backup', '0');
        PVE.Utils.propertyStringSet(me.drive, values.noreplicate, 'replicate', 
'no');
        PVE.Utils.propertyStringSet(me.drive, values.discard, 'discard', 'on');
@@ -90,15 +128,22 @@ Ext.define('PVE.qemu.HDInputPanel', {
            PVE.Utils.propertyStringSet(me.drive, values[name], name);
            PVE.Utils.propertyStringSet(me.drive, values[burst_name], 
burst_name);
        });
-       if (me.isImport) {
+
+       if (me.returnSingleKey) {
+           if (me.isImport) {
+               me.drive.importsource = this.getSourceValue(values);
+           }
+           params[confid] = PVE.Parser.printQemuDrive(me.drive);
+       } else {
+           console.assert(me.isImport,
+               "Returning multiple key/values is only allowed in import");
            params.device_options = PVE.Parser.printPropertyString(me.drive);
-           params.source = values.sourceType === 'storage'
-               ? values.sourceVolid : values.sourcePath;
+           params.source = this.getSourceValue(values);
            params.device = values.controller + values.deviceid;
            params.storage = values.hdstorage;
-           if (values.diskformat) params.format = values.diskformat;
-       } else {
-           params[confid] = PVE.Parser.printQemuDrive(me.drive);
+           if (values.diskformat) {
+               params.format = values.diskformat;
+           }
        }
        return params;
     },
@@ -156,10 +201,16 @@ 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);
        me.down('#hdimage').setStorage(undefined, nodename);
+       // me.down('#sourceStorageSelector').setNodename(nodename);
+       // me.down('#sourceFileSelector').setNodename(nodename);
     },
 
     initComponent : function() {
@@ -175,12 +226,16 @@ Ext.define('PVE.qemu.HDInputPanel', {
        me.advancedColumn1 = [];
        me.advancedColumn2 = [];
 
+       let nodename = this.getViewModel().get('nodename'); // TODO hacky whacky
+
+
        if (!me.confid || me.unused) {
-           let controllerColumn = me.isImport ? me.column2 : me.column1;
+           let controllerColumn = me.showSourcePathTextfield ? me.column2 : 
me.column1;
            me.bussel = Ext.create('PVE.form.ControllerSelector', {
+               itemId: 'bussel',
                vmconfig: me.insideWizard ? {ide2: 'cdrom'} : {}
            });
-           if (me.isImport) {
+           if (me.showSourcePathTextfield) {
                me.bussel.fieldLabel = 'Target Device';
            }
            controllerColumn.push(me.bussel);
@@ -210,16 +265,16 @@ Ext.define('PVE.qemu.HDInputPanel', {
                allowBlank: false
            });
            me.column1.push(me.unusedDisks);
-       } else if (me.isCreate || me.isImport) {
+       } else if (me.isCreate || me.showSourcePathTextfield) {
            let selector = {
                xtype: 'pveDiskStorageSelector',
                storageContent: 'images',
                name: 'disk',
-               nodename: me.nodename,
-               hideSize: me.isImport,
-               autoSelect: me.insideWizard || me.isImport,
+               nodename: nodename,
+               hideSize: me.showSourcePathTextfield,
+               autoSelect: me.insideWizard || me.showSourcePathTextfield,
            };
-           if (me.isImport) {
+           if (me.showSourcePathTextfield) {
                selector.storageLabel = gettext('Target storage');
                me.column2.push(selector);
            } else {
@@ -235,7 +290,7 @@ Ext.define('PVE.qemu.HDInputPanel', {
            });
        }
 
-       if (me.isImport) {
+       if (me.showSourcePathTextfield) {
            me.column2.push({
                xtype: 'box',
                autoEl: { tag: 'hr' },
@@ -255,72 +310,83 @@ Ext.define('PVE.qemu.HDInputPanel', {
                name: 'discard'
            }
        );
-       if (me.isImport) {
+       if (me.showSourcePathTextfield) {
            let show = (element, value) => {
                element.setHidden(!value);
                element.setDisabled(!value);
            };
-           me.sourceRadioStorage = Ext.create('Ext.form.field.Radio', {
-               name: 'sourceType',
-               inputValue: 'storage',
-               boxLabel: gettext('Use a storage as source'),
-               checked: true,
-               hidden: Proxmox.UserName !== 'root@pam',
-               listeners: {
-                   added: () => show(me.sourcePathTextfield, false),
-                   change: (_, storageRadioChecked) => {
-                       show(me.sourcePathTextfield, !storageRadioChecked);
-                       let selectors = [
-                           me.sourceStorageSelector,
-                           me.sourceFileSelector,
-                       ];
-                       for (const selector of selectors) {
-                           show(selector, storageRadioChecked);
-                       }
+
+           me.column1.unshift(
+               {
+                   xtype: 'radiofield',
+                   itemId: 'sourceRadioStorage',
+                   name: me.getSourceTypeIdentifier(),
+                   inputValue: 'storage',
+                   boxLabel: gettext('Use a storage as source'),
+                   hidden: Proxmox.UserName !== 'root@pam',
+                   checked: true,
+                   listeners: {
+                       change: (_, newValue) => {
+                           let storageSelectors = [
+                               me.down('#sourceStorageSelector'),
+                               me.down('#sourceFileSelector'),
+                           ];
+                           for (const selector of storageSelectors) {
+                               show(selector, newValue);
+                           }
+                       },
                    },
-               },
-           });
-           me.sourceStorageSelector = Ext.create('PVE.form.StorageSelector', {
-               name: 'inputImageStorage',
-               nodename: me.nodename,
-               fieldLabel: gettext('Source Storage'),
-               storageContent: 'images',
-               autoSelect: me.insideWizard,
-               listeners: {
-                   change: function(_, selectedStorage) {
-                       me.sourceFileSelector.setStorage(selectedStorage);
+               }, {
+                   xtype: 'pveStorageSelector',
+                   itemId: 'sourceStorageSelector',
+                   name: 'inputImageStorage',
+                   nodename: 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', // TODO scope of itemId is container, 
this breaks onGetValues, only one thingy is selected for multiple inputpanels
+                   nodename: nodename,
+                   storageContent: 'images',
+                   hidden: true,
+                   disabled: true,
+                   fieldLabel: gettext('Source Image'),
+               }, {
+                   xtype: 'radiofield',
+                   itemId: 'sourceRadioPath',
+                   name: me.getSourceTypeIdentifier(),
+                   inputValue: 'path',
+                   boxLabel: gettext('Use an absolute path as source'),
+                   hidden: Proxmox.UserName !== 'root@pam',
+                   listeners: {
+                       change: (_, newValue) => {
+                           show(me.down('#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: (insertedText) =>
+                       insertedText.startsWith('/') ||
+                           gettext('Must be an absolute path'),
                },
-           });
-           me.sourceFileSelector = Ext.create('PVE.form.FileSelector', {
-               name: 'sourceVolid',
-               nodename: me.nodename,
-               storageContent: 'images',
-               fieldLabel: gettext('Source Image'),
-           });
-           me.sourceRadioPath = Ext.create('Ext.form.field.Radio', {
-               name: 'sourceType',
-               inputValue: 'path',
-               boxLabel: gettext('Use an absolute path as source'),
-               hidden: Proxmox.UserName !== 'root@pam',
-           });
-           me.sourcePathTextfield = Ext.create('Ext.form.field.Text', {
-               xtype: 'textfield',
-               fieldLabel: gettext('Source Path'),
-               name: 'sourcePath',
-               emptyText: '/home/user/disk.qcow2',
-               hidden: Proxmox.UserName !== 'root@pam',
-               validator: function(insertedText) {
-                   return insertedText.startsWith('/') ||
-                       gettext('Must be an absolute path');
-               },
-           });
-           me.column1.unshift(
-               me.sourceRadioStorage,
-               me.sourceStorageSelector,
-               me.sourceFileSelector,
-               me.sourceRadioPath,
-               me.sourcePathTextfield,
            );
        }
 
@@ -465,7 +531,8 @@ Ext.define('PVE.qemu.HDEdit', {
            nodename: nodename,
            unused: unused,
            isCreate: me.isCreate,
-           isImport: me.isImport,
+           showSourcePathTextfield: me.isImport,
+           returnSingleKey: !me.isImport,
        });
 
        var subject;
diff --git a/www/manager6/qemu/ImportWizard.js 
b/www/manager6/qemu/ImportWizard.js
new file mode 100644
index 00000000..c6e91a48
--- /dev/null
+++ b/www/manager6/qemu/ImportWizard.js
@@ -0,0 +1,379 @@
+/*jslint confusion: true*/
+Ext.define('PVE.qemu.ImportWizard', {
+    extend: 'PVE.window.Wizard',
+    alias: 'widget.pveQemuImportWizard',
+    mixins: ['Proxmox.Mixin.CBind'],
+
+    viewModel: {
+       data: {
+           nodename: '',
+           current: {
+               scsihw: '' // TODO there is some error with apply after 
render_scsihw??
+           }
+       }
+    },
+
+    cbindData: {
+       nodename: undefined
+    },
+
+    subject: gettext('Import Virtual Machine'),
+
+    isImport: true,
+
+    addDiskFunction: function () {
+       let me = this;
+       let wizard;
+       if (me.xtype === 'button') {
+               wizard = me.up('window');
+       } else if (me.xtype === 'pveQemuImportWizard') {
+               wizard = me;
+       }
+       console.assert(wizard.xtype === 'pveQemuImportWizard');
+       let multihd = wizard.down('pveQemuMultiHDInputPanel');
+       multihd.addDiskFunction();
+    },
+
+    items: [
+       {
+               xtype: 'inputpanel',
+               title: gettext('Import'),
+               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: [
+                       // { // TODO implement the rest
+                       //      xtype: 'filebutton',
+                       //      text: gettext('Load local manifest ...'),
+                       //      allowBlank: true,
+                       //      hidden: Proxmox.UserName !== 'root@pam',
+                       //      disabled: Proxmox.UserName !== 'root@pam',
+                       //      listeners: {
+                       //              change: (button,event,) => {
+                       //                      var reader = new FileReader();
+                       //                      let wizard = 
button.up('window');
+                       //                      reader.onload = (e) => {
+                       //                              let uploaded_ovf = 
e.target.result;
+                       //                              // TODO set fields here
+                       //                              // TODO When to upload 
disks to server?
+                       //                      };
+                       //                      
reader.readAsText(event.target.files[0]);
+                       //                      button.disable(); // TODO 
implement complete reload
+                       //                      
wizard.down('#successTextfield').show();
+                       //              }
+                       //      }
+                       // },
+                       {
+                               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',
+                               value: 
'/mnt/pve/cifs/importing/ovf_from_hyperv/pve/pve.ovf',
+                               emptyText: '/mnt/nfs/exported.ovf',
+                               fieldLabel: 'Absolute path to .ovf manifest on 
your PVE host',
+                       },
+                       {
+                               xtype: 'proxmoxButton',
+                               text: gettext('Load remote manifest'),
+                               handler: function() {
+                                       let panel = this.up('panel'); 
+                                       let nodename = 
panel.down('pveNodeSelector').getValue();
+                                        // independent of onGetValues(), so 
that value of
+                                        // ovf_textfield can be removed for 
submit
+                                       let ovf_textfield_value = 
panel.down('#server_ovf_manifest').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) 
{
+                                                       let path = 
ovfdata[device].split('=')[1];
+                                                       
multihd.addDiskFunction(device, path);
+                                                   }
+                                               },
+                                               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: 'Hard Disk',
+       },
+       {
+           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 kv = wizard.getValues();
+               delete kv['delete'];
+
+               var nodename = kv.nodename;
+               delete kv.nodename;
+
+               Proxmox.Utils.API2Request({
+                   url: '/nodes/' + nodename + '/qemu',
+                   waitMsgTarget: wizard,
+                   method: 'POST',
+                   params: kv,
+                   success: function(response){
+                       wizard.close();
+                   },
+                   failure: function(response, opts) {
+                       Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+                   }
+               });
+           }
+       }
+       ],
+    initComponent: function () {
+       var me = this;
+       me.callParent();
+
+       let addDiskButton = {
+               text: gettext('Add disk'),
+               disabled: true,
+               itemId: 'addDisk',
+               minWidth: 60,
+               handler: me.addDiskFunction,
+               isValid: function () {
+                       let isValid = true;
+                       if (!me.isImport) {
+                               isValid = false;
+                       }
+                       let type = me.down('#wizcontent').getActiveTab().xtype;
+                       if (type !== 'pveQemuHDInputPanel') {
+                               isValid=false;
+                       }
+                       return isValid;
+               },
+       };
+
+       let removeDiskButton = {
+           text: gettext('Remove disk'), // TODO implement
+           disabled: false,
+           itemId: 'removeDisk',
+           minWidth: 60,
+           handler: function() {
+               console.assert(me.xtype === 'pveQemuImportWizard');
+               let multihd = me.down('pveQemuMultiHDInputPanel');
+               multihd.removeCurrentDisk();
+           },
+       };
+       me.down('toolbar').insert(4, addDiskButton);
+       me.down('toolbar').insert(5, removeDiskButton);
+    },
+});
+
+
+
+
diff --git a/www/manager6/qemu/MultiHDEdit.js b/www/manager6/qemu/MultiHDEdit.js
new file mode 100644
index 00000000..632199ba
--- /dev/null
+++ b/www/manager6/qemu/MultiHDEdit.js
@@ -0,0 +1,267 @@
+Ext.define('PVE.qemu.MultiHDInputPanel', {
+    extend: 'Proxmox.panel.InputPanel',
+    alias: 'widget.pveQemuMultiHDInputPanel',
+
+    insideWizard: false,
+
+    hiddenDisks: [],
+
+    leftColumnRatio: 0.2,
+
+    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) {
+                   console.assert(panel.xtype === 'pveQemuHDInputPanel');
+                   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% 100%', // Required because resize does not happen yet
+       },
+    ],
+    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 previousCount = store.data.getCount();
+                   let indexOfRemoved = store.removeByPanel(HDInputPanel);
+                   console.assert(store.data.getCount() === previousCount - 1,
+                       'Nothing has been removed from the store.' +
+                       `It still has ${store.data.getCount()} items.`,
+                   );
+                   if (HDInputPanelContainer.items.getCount() > 0) {
+                       console.assert(indexOfRemoved >= 1);
+                       HDInputPanelContainer.setActiveItem(indexOfRemoved - 1);
+                   }
+               },
+           },
+           defaultItem: {
+               xtype: 'pveQemuHDInputPanel',
+               bind: {
+                   nodename: '{nodename}',
+                   viewModel: '{viewModel}',
+               },
+               isCreate: true,
+               isImport: true,
+               showSourcePathTextfield: true,
+               returnSingleKey: true,
+               insideWizard: true,
+               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'),
+                                   );
+                                   console.assert(
+                                       newHDInputPanel === 
field.up('pveQemuHDInputPanel'),
+                                       'Those panels should be the same',
+                                   );
+                                   console.assert(recordIndex !== -1);
+                                   let newControllerAndId = 
controllerSelector.getValuesAsString();
+                                   store.getAt(recordIndex).set('device', 
newControllerAndId);
+                               });
+                           },
+                       );
+                   },
+                   beforerender: function() {
+                       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() {
+                   console.debug('hdedit validator');
+                   var valid = true;
+                   var fields = this.query('field, fieldcontainer');
+                   if (this.isXType('fieldcontainer')) {
+                       console.assert(false);
+                       fields.unshift(this);
+                   }
+                   Ext.Array.each(fields, function(field) {
+                       // Note: not all fielcontainer have isValid()
+                       if (Ext.isFunction(field.isValid) && !field.isValid()) {
+                           valid = false;
+                           console.debug('field is invalid');
+                           console.debug(field);
+                       }
+                   });
+                   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();
+               }
+               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);
+               } else {
+                       // TODO Add tooltip to Remove disk button
+               }
+           },
+       },
+    ],
+
+    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() {
+       // any other panel because this has no height yet
+       let panelHeight = this.up('tabpanel').items.items[0].getHeight();
+       let leftColumnContainer = this.items.items[0];
+       let rightColumnContainer = this.items.items[1];
+       leftColumnContainer.setHeight(panelHeight);
+
+       leftColumnContainer.columnWidth = this.leftColumnRatio;
+       rightColumnContainer.columnWidth = 1 - this.leftColumnRatio;
+    },
+
+    // Call with defined parameter or without (static function so to say)
+    hasDuplicateDevices: function(values) {
+       if (!values) {
+           values = this.up('form').getValues();
+       }
+       console.assert(values);
+       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 87e4bf0a..f16ba107 100644
--- a/www/manager6/window/Wizard.js
+++ b/www/manager6/window/Wizard.js
@@ -35,6 +35,75 @@ Ext.define('PVE.window.Wizard', {
         return values;
     },
 
+    check_card: function(card) {
+       var valid = true;
+       var fields = card.query('field, fieldcontainer');
+       if (card.isXType('fieldcontainer')) {
+           fields.unshift(card);
+       }
+       Ext.Array.each(fields, function(field) {
+           // Note: not all fielcontainer have isValid()
+           if (Ext.isFunction(field.isValid) && !field.isValid()) {
+               valid = false;
+           }
+       });
+
+       if (Ext.isFunction(card.validator)) {
+           return card.validator();
+       }
+
+       return valid;
+    },
+
+    disable_at: function(card) {
+       let window = this;
+       var topbar = window.down('#wizcontent');
+       var idx = topbar.items.indexOf(card);
+       for(;idx < topbar.items.getCount();idx++) {
+           var nc = topbar.items.getAt(idx);
+           if (nc) {
+               nc.disable();
+           }
+       }
+    },
+
+    validcheck: function() {
+       console.debug('Validcheck');
+       let window = this.up('window');
+       var topbar = window.down('#wizcontent');
+
+       // check tabs from current to the last enabled for validity
+       // since we might have changed a validity on a later one
+       var i;
+       for (i = topbar.curidx; i <= topbar.maxidx && i < 
topbar.items.getCount(); i++) {
+           var tab = topbar.items.getAt(i);
+           var valid = window.check_card(tab);
+
+           // only set the buttons on the current panel
+           if (i === topbar.curidx) {
+               if (window.isImport) {
+                   console.debug('valid in window?');
+                   console.debug(valid);
+                   console.debug('because tab is');
+                   console.debug(tab);
+                   window.down('#addDisk').setDisabled(!valid);
+               }
+               window.down('#next').setDisabled(!valid);
+               window.down('#submit').setDisabled(!valid);
+           }
+
+           // if a panel is invalid, then disable it and all following,
+           // else enable it and go to the next
+           var ntab = topbar.items.getAt(i + 1);
+           if (!valid) {
+               window.disable_at(ntab);
+               return;
+           } else if (ntab && !tab.onSubmit) {
+               ntab.enable();
+           }
+       }
+    },
+
     initComponent: function() {
        var me = this;
 
@@ -53,40 +122,6 @@ Ext.define('PVE.window.Wizard', {
        });
        tabs[0].disabled = false;
 
-       var maxidx = 0;
-       var curidx = 0;
-
-       var check_card = function(card) {
-           var valid = true;
-           var fields = card.query('field, fieldcontainer');
-           if (card.isXType('fieldcontainer')) {
-               fields.unshift(card);
-           }
-           Ext.Array.each(fields, function(field) {
-               // Note: not all fielcontainer have isValid()
-               if (Ext.isFunction(field.isValid) && !field.isValid()) {
-                   valid = false;
-               }
-           });
-
-           if (Ext.isFunction(card.validator)) {
-               return card.validator();
-           }
-
-           return valid;
-       };
-
-       var disable_at = function(card) {
-           var tp = me.down('#wizcontent');
-           var idx = tp.items.indexOf(card);
-           for(;idx < tp.items.getCount();idx++) {
-               var nc = tp.items.getAt(idx);
-               if (nc) {
-                   nc.disable();
-               }
-           }
-       };
-
        var tabchange = function(tp, newcard, oldcard) {
            if (newcard.onSubmit) {
                me.down('#next').setVisible(false);
@@ -95,16 +130,23 @@ Ext.define('PVE.window.Wizard', {
                me.down('#next').setVisible(true);
                me.down('#submit').setVisible(false); 
            }
-           var valid = check_card(newcard);
+           var valid = me.check_card(newcard);
+           let addDiskButton = me.down('#addDisk'); // TODO undefined in first 
invocation?
+           if (me.isImport && addDiskButton) {
+               addDiskButton.setDisabled(!valid); // TODO check me
+               addDiskButton.setHidden(!addDiskButton.isValid());
+               addDiskButton.setDisabled(false);
+               addDiskButton.setHidden(false);
+           }
            me.down('#next').setDisabled(!valid);    
            me.down('#submit').setDisabled(!valid);    
            me.down('#back').setDisabled(tp.items.indexOf(newcard) == 0);
 
            var idx = tp.items.indexOf(newcard);
-           if (idx > maxidx) {
-               maxidx = idx;
+           if (idx > tp.maxidx) {
+               tp.maxidx = idx;
            }
-           curidx = idx;
+           tp.curidx = idx;
 
            var next = idx + 1;
            var ntab = tp.items.getAt(next);
@@ -135,6 +177,8 @@ Ext.define('PVE.window.Wizard', {
                    items: [{
                        itemId: 'wizcontent',
                        xtype: 'tabpanel',
+                       maxidx: 0,
+                       curidx: 0,
                        activeItem: 0,
                        bodyPadding: 10,
                        listeners: {
@@ -201,7 +245,7 @@ Ext.define('PVE.window.Wizard', {
 
                        var tp = me.down('#wizcontent');
                        var atab = tp.getActiveTab();
-                       if (!check_card(atab)) {
+                       if (!me.check_card(atab)) {
                            return;
                        }
 
@@ -234,35 +278,8 @@ Ext.define('PVE.window.Wizard', {
        });
 
        Ext.Array.each(me.query('field'), function(field) {
-           var validcheck = function() {
-               var tp = me.down('#wizcontent');
-
-               // check tabs from current to the last enabled for validity
-               // since we might have changed a validity on a later one
-               var i;
-               for (i = curidx; i <= maxidx && i < tp.items.getCount(); i++) {
-                   var tab = tp.items.getAt(i);
-                   var valid = check_card(tab);
-
-                   // only set the buttons on the current panel
-                   if (i === curidx) {
-                       me.down('#next').setDisabled(!valid);
-                       me.down('#submit').setDisabled(!valid);
-                   }
-
-                   // if a panel is invalid, then disable it and all following,
-                   // else enable it and go to the next
-                   var ntab = tp.items.getAt(i + 1);
-                   if (!valid) {
-                       disable_at(ntab);
-                       return;
-                   } else if (ntab && !tab.onSubmit) {
-                       ntab.enable();
-                   }
-               }
-           };
-           field.on('change', validcheck);
-           field.on('validitychange', validcheck);
+           field.on('change', me.validcheck);
+           field.on('validitychange', me.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