hi,

tested along with the qemu-server patch, it seems to work but had some
issues in some cases (especially with windows VMs)

we discussed already off-list with dominic during the testing, but i'll
just put these down here anyway:

- the "Next" button should be grayed out until remote manifest is
loaded, or alternatively attempt to load manifest when next button is
pressed (instead of having a separate button)

- in the disk menu, pressing the "+" adds a disk but doesn't increment
the id, which can lead to having multiple disks with the same name

- windows VMs need some special treatment when setting config options:
-> machine: q35
-> use sata disk instead of ide
-> also UEFI can be made default? in my tests the windows 10
installation didn't boot until adding a EFI disk and changing the BIOS
to OVMF


but besides these, it looks okay to me, and works as advertised :)





On Mon, Apr 12, 2021 at 12:08:25PM +0200, Dominic Jäger wrote:
> 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>
> ---
> v8:
> - Adapt to new API
> - Some small fixes
> - Much renaming
> 
>  PVE/API2/Nodes.pm                       |   7 +
>  www/manager6/Makefile                   |   2 +
>  www/manager6/Workspace.js               |  15 ++
>  www/manager6/form/ControllerSelector.js |  15 ++
>  www/manager6/node/CmdMenu.js            |  13 +
>  www/manager6/qemu/HDEdit.js             | 149 ++++++++++-
>  www/manager6/qemu/HDEditCollection.js   | 263 ++++++++++++++++++++
>  www/manager6/qemu/HardwareView.js       |  24 ++
>  www/manager6/qemu/ImportWizard.js       | 317 ++++++++++++++++++++++++
>  www/manager6/window/Wizard.js           |   2 +
>  10 files changed, 795 insertions(+), 12 deletions(-)
>  create mode 100644 www/manager6/qemu/HDEditCollection.js
>  create mode 100644 www/manager6/qemu/ImportWizard.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..dbb85062 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/HDEditCollection.js                                \
>       qemu/HDEfi.js                                   \
>       qemu/HDMove.js                                  \
>       qemu/HDResize.js                                \
> 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..f515b220 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) {
> +     const regex = /([a-z]+)(\d+)/;
> +     const [_, 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..b66c7a6e 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() {
> +             const me = this.up('menu');
> +             const 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..3af7e624 100644
> --- a/www/manager6/qemu/HDEdit.js
> +++ b/www/manager6/qemu/HDEdit.js
> @@ -58,6 +58,17 @@ 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 HDInputPanelCollection 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 +81,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}:-1`;
>           } else {
>               me.drive.file = values.hdstorage + ":" + values.disksize;
>           }
> @@ -83,13 +96,21 @@ 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.isImport) {
> +         params.import_sources = `${confid}=${getSourceImageLocation()}`;
> +     }
>  
>       params[confid] = PVE.Parser.printQemuDrive(me.drive);
>  
> @@ -149,6 +170,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 +194,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 +214,7 @@ Ext.define('PVE.qemu.HDInputPanel', {
>               submitValue: false,
>               hidden: true,
>           });
> -         me.column1.push(me.scsiController);
> +         controllerColumn.push(me.scsiController);
>       }
>  
>       if (me.unused) {
> @@ -199,14 +229,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 +254,12 @@ Ext.define('PVE.qemu.HDInputPanel', {
>           });
>       }
>  
> +     if (me.isImport) {
> +         me.column2.push({
> +             xtype: 'box',
> +             autoEl: { tag: 'hr' },
> +         });
> +     }
>       me.column2.push(
>           {
>               xtype: 'CacheTypeSelector',
> @@ -231,6 +274,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) => {
> +                         
> me.down('#sourceStorageSelector').setHidden(!newValue);
> +                         
> me.down('#sourceStorageSelector').setDisabled(!newValue);
> +                         me.down('#sourceFileSelector').setHidden(!newValue);
> +                         
> me.down('#sourceFileSelector').setDisabled(!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) => {
> +                         
> me.down('#sourcePathTextfield').setHidden(!newValue);
> +                         
> me.down('#sourcePathTextfield').setDisabled(!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 +494,17 @@ Ext.define('PVE.qemu.HDEdit', {
>           nodename: nodename,
>           unused: unused,
>           isCreate: me.isCreate,
> +         isImport: me.isImport,
>       });
>  
> -     var subject;
>       if (unused) {
>           me.subject = gettext('Unused Disk');
> +     } else if (me.isImport) {
> +         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 + ')';
>       }
> diff --git a/www/manager6/qemu/HDEditCollection.js 
> b/www/manager6/qemu/HDEditCollection.js
> new file mode 100644
> index 00000000..33f6193a
> --- /dev/null
> +++ b/www/manager6/qemu/HDEditCollection.js
> @@ -0,0 +1,263 @@
> +Ext.define('PVE.qemu.HDInputPanelCollection', {
> +    extend: 'Proxmox.panel.InputPanel',
> +    alias: 'widget.pveQemuHDInputPanelCollection',
> +
> +    insideWizard: false,
> +
> +    hiddenDisks: [],
> +
> +    leftColumnRatio: 0.25,
> +
> +    column1: [
> +     {
> +         // Adding to the panelContainer 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) {
> +                 const recordIndex = this.findBy(record => record.data.panel 
> === panel);
> +                 this.removeAt(recordIndex);
> +                 return recordIndex;
> +             },
> +             getLast: function() {
> +                 const last = this.getCount() - 1;
> +                 return this.getAt(last);
> +             },
> +         },
> +         columns: [
> +             {
> +                 text: gettext('Target device'),
> +                 dataIndex: 'device',
> +                 flex: 1,
> +                 resizable: false,
> +             },
> +         ],
> +         listeners: {
> +             select: function(_, record) {
> +                 this.up('pveQemuHDInputPanelCollection')
> +                     .down('#panelContainer')
> +                     .setActiveItem(record.data.panel);
> +             },
> +         },
> +         anchor: '100% 90%',
> +         selectLast: function() {
> +             this.setSelection(this.store.getLast());
> +         },
> +     }, {
> +         xtype: 'container',
> +         layout: 'hbox',
> +         center: true,
> +         defaults: {
> +             margin: '5',
> +             xtype: 'button',
> +         },
> +         items: [
> +             {
> +                 iconCls: 'fa fa-plus-circle',
> +                 itemId: 'addDisk',
> +                 handler: function(button) {
> +                     button.up('pveQemuHDInputPanelCollection').addDisk();
> +                 },
> +             }, {
> +                 iconCls: 'fa fa-trash-o',
> +                 itemId: 'removeDisk',
> +                 handler: function(button) {
> +                     
> button.up('pveQemuHDInputPanelCollection').removeCurrentDisk();
> +                 },
> +             },
> +         ],
> +     },
> +    ],
> +    column2: [
> +     {
> +         itemId: 'panelContainer',
> +         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.addDisk();
> +                 }
> +             },
> +             add: function(container, newPanel) {
> +                 const store = Ext.getStore('importwizard_diskstorage');
> +                 store.add({ device: newPanel.getDevice(), panel: newPanel 
> });
> +                 container.setActiveItem(newPanel);
> +             },
> +             remove: function(panelContainer, HDInputPanel, eOpts) {
> +                 const store = Ext.getStore('importwizard_diskstorage');
> +                 store.removeByPanel(HDInputPanel);
> +                 if (panelContainer.items.getCount() > 0) {
> +                     panelContainer.setActiveItem(0);
> +                 }
> +             },
> +         },
> +         defaultItem: {
> +             xtype: 'pveQemuHDInputPanel',
> +             bind: {
> +                 nodename: '{nodename}',
> +             },
> +             isCreate: true,
> +             isImport: 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: {
> +                 // newPanel ... this cloned + added defaultItem
> +                 added: function(newPanel) {
> +                     
> Ext.Array.each(newPanel.down('pveControllerSelector').query('field'),
> +                         function(field) {
> +                             // Add here because the fields don't exist 
> earlier
> +                             field.on('change', function() {
> +                                 const store = 
> Ext.getStore('importwizard_diskstorage');
> +
> +                                 // find by panel object because it is unique
> +                                 const recordIndex = store.findBy(record =>
> +                                     record.data.panel === 
> field.up('pveQemuHDInputPanel'),
> +                                 );
> +                                 const controllerSelector = 
> field.up('pveControllerSelector');
> +                                 const newControllerAndId = 
> controllerSelector.getValuesAsString();
> +
> +                                 store.getAt(recordIndex).set('device', 
> newControllerAndId);
> +                             });
> +                         },
> +                     );
> +                     const 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 ... content of the sourcePathTextfield
> +         addDisk(device, path) {
> +             const item = Ext.clone(this.defaultItem);
> +             const added = this.add(item);
> +             // values in the storage will be updated by listeners
> +             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();
> +             }
> +
> +             const sp = Ext.state.Manager.getProvider();
> +             const advanced_checkbox = sp.get('proxmox-advanced-cb');
> +             added.setAdvancedVisible(advanced_checkbox);
> +
> +             if (device) {
> +                 added.down('pveControllerSelector').setValue(device);
> +             }
> +             return added;
> +         },
> +         removeCurrentDisk: function() {
> +             const activePanel = this.getLayout().activeItem; // panel = disk
> +             if (activePanel) {
> +                 this.remove(activePanel);
> +             }
> +         },
> +     },
> +    ],
> +
> +    addDisk: function(device, path) {
> +     this.down('#panelContainer').addDisk(device, path);
> +     this.down('gridpanel').selectLast();
> +    },
> +    removeCurrentDisk: function() {
> +     this.down('#panelContainer').removeCurrentDisk();
> +    },
> +    removeAllDisks: function() {
> +     const container = this.down('#panelContainer');
> +     while (container.items.items.length > 0) {
> +         container.removeCurrentDisk();
> +     }
> +    },
> +
> +    beforeRender: function() {
> +     const me = this;
> +     const leftColumnPanel = me.items.get(0).items.get(0);
> +     leftColumnPanel.setFlex(me.leftColumnRatio);
> +     // any other panel because this has no height yet
> +     const panelHeight = me.up('tabpanel').items.get(0).getHeight();
> +     leftColumnPanel.setHeight(panelHeight);
> +    },
> +
> +    setNodename: function(nodename) {
> +     this.nodename = nodename;
> +    },
> +
> +    listeners: {
> +     afterrender: function() {
> +         const store = Ext.getStore('importwizard_diskstorage');
> +         const first = store.getAt(0);
> +         if (first) {
> +             this.down('gridpanel').setSelection(first);
> +         }
> +     },
> +    },
> +
> +    // values ... is optional
> +    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] &&
> +                 values.deviceid[i] === values.deviceid[j]
> +             ) {
> +                 return true;
> +             }
> +         }
> +     }
> +     return false;
> +    },
> +
> +    onGetValues: function(values) {
> +     if (this.hasDuplicateDevices(values)) {
> +         Ext.Msg.alert(gettext('Error'), 'Equal target devices are 
> forbidden. Make all unique!');
> +     }
> +     // Each child HDInputPanel has sufficient onGetValues() => Return 
> nothing
> +    },
> +
> +    validator: function() {
> +     const me = this;
> +     const panels = me.down('#panelContainer').items.getRange();
> +     return panels.every(panel => panel.validator()) && 
> !me.hasDuplicateDevices();
> +    },
> +});
> diff --git a/www/manager6/qemu/HardwareView.js 
> b/www/manager6/qemu/HardwareView.js
> index 98352e3f..be4e2d28 100644
> --- a/www/manager6/qemu/HardwareView.js
> +++ b/www/manager6/qemu/HardwareView.js
> @@ -431,6 +431,29 @@ 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,
> +                 isImport: true,
> +                 listeners: {
> +                     add: function(_, component) {
> +                         component.down('#sourceStorageSelector').show();
> +                         component.down('#sourceStorageSelector').enable();
> +                         component.down('#sourceFileSelector').enable();
> +                         component.down('#sourceFileSelector').show();
> +                     },
> +                 },
> +             });
> +             win.on('destroy', me.reload, me);
> +             win.show();
> +         },
> +     });
> +
>       var remove_btn = new Proxmox.button.Button({
>           text: gettext('Remove'),
>           defaultText: gettext('Remove'),
> @@ -759,6 +782,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..a9a63fe3
> --- /dev/null
> +++ b/www/manager6/qemu/ImportWizard.js
> @@ -0,0 +1,317 @@
> +/*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'),
> +
> +    isImport: true,
> +
> +    addDisk: function() {
> +     const me = this;
> +     const wizard = me.xtype === 'pveQemuImportWizard' ? me : 
> me.up('window');
> +     wizard.down('pveQemuHDInputPanelCollection').addDisk();
> +    },
> +
> +    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',
> +                 name: 'ovfTextfield',
> +                 emptyText: '/mnt/nfs/exported.ovf',
> +                 fieldLabel: 'Absolute path to .ovf manifest on your PVE 
> host',
> +                 listeners: {
> +                     validitychange: function(_, isValid) {
> +                         const 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() {
> +                     const inputpanel = this.up('#importInputpanel');
> +                     const nodename = 
> inputpanel.down('pveNodeSelector').getValue();
> +                     const ovfTextfieldValue = 
> inputpanel.down('textfield[name=ovfTextfield]').getValue();
> +                     const wizard = this.up('window');
> +                     Proxmox.Utils.API2Request({
> +                         url: '/nodes/' + nodename + '/readovf',
> +                         method: 'GET',
> +                         params: {
> +                             manifest: ovfTextfieldValue,
> +                         },
> +                         success: function(response) {
> +                             const 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;
> +                             const devices = Object.keys(ovfdata); // e.g. 
> ide0, sata2
> +                             const hdcollection = 
> wizard.down('pveQemuHDInputPanelCollection');
> +                             hdcollection.removeAllDisks(); // does nothing 
> if already empty
> +                             devices.forEach(device => 
> hdcollection.addDisk(device, ovfdata[device]));
> +                         },
> +                         failure: function(response) {
> +                             Ext.Msg.alert(gettext('Error'), 
> response.htmlStatus);
> +                         },
> +                     });
> +                 },
> +             },
> +         ],
> +         onGetValues: function(values) {
> +             delete values.ovfTextfield;
> +             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: 'pveQemuHDInputPanelCollection',
> +         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.import_sources)) {
> +                 params.import_sources = params.import_sources.join('\0');
> +             }
> +
> +             Proxmox.Utils.API2Request({
> +                 url: `/nodes/${nodename}/qemu`,
> +                 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/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


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

Reply via email to