[pve-devel] [PATCH] Add GUI to import disk & VM
Dominic Jäger
d.jaeger at proxmox.com
Mon Apr 12 12:08:25 CEST 2021
Add GUI wizard to import whole VMs and a window to import single disks in
Hardware View.
Signed-off-by: Dominic Jäger <d.jaeger at 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 at 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 at 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 at 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 at 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 at 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
More information about the pve-devel
mailing list