This is only a draft! Please comment! * Added a CloudInit tab with network configuration * The 'Generate' is renamed to 'Regenerate' when a cloudinit image exists (regenerate functionality not yet implemented). * Deletion of the cloudinit image from out of the CloudInit tab defaults to using the 'force' flag to avoid leaving 'unused' disks around. * Added a PVE.Utils.forEachBus to not duplicate all bus names and counts. --- www/css/ext-pve.css | 6 + www/images/Makefile | 1 + www/images/cloudinit.png | Bin 0 -> 193 bytes www/manager/Makefile | 3 + www/manager/Parser.js | 57 +++++++ www/manager/Utils.js | 27 +++- www/manager/qemu/CloudInit.js | 296 +++++++++++++++++++++++++++++++++++ www/manager/qemu/CloudInitCreator.js | 84 ++++++++++ www/manager/qemu/Config.js | 5 + www/manager/qemu/HardwareView.js | 52 ++---- www/manager/qemu/IPConfigEdit.js | 234 +++++++++++++++++++++++++++ 11 files changed, 728 insertions(+), 37 deletions(-) create mode 100644 www/images/cloudinit.png create mode 100644 www/manager/qemu/CloudInit.js create mode 100644 www/manager/qemu/CloudInitCreator.js create mode 100644 www/manager/qemu/IPConfigEdit.js
diff --git a/www/css/ext-pve.css b/www/css/ext-pve.css index 238c30e..461e22a 100644 --- a/www/css/ext-pve.css +++ b/www/css/ext-pve.css @@ -55,6 +55,7 @@ .pve-itype-icon-node-running, .pve-itype-icon-storage, .pve-itype-icon-pool, +.pve-itype-icon-cloudinit, .pve-itype-icon-itype { background-repeat: no-repeat; @@ -186,6 +187,11 @@ background-image:url(../images/virt-viewer.png); } +.pve-itype-icon-cloudinit +{ + background-image:url(../images/cloudinit.png); +} + .pve-bar-wrap { diff --git a/www/images/Makefile b/www/images/Makefile index 0a6cf85..5bd27bf 100644 --- a/www/images/Makefile +++ b/www/images/Makefile @@ -24,6 +24,7 @@ GNOME_IMAGES = \ keyboard.png \ cdrom.png \ network.png \ + cloudinit.png \ drive-harddisk.png \ network-server.png \ connect_established.png \ diff --git a/www/images/cloudinit.png b/www/images/cloudinit.png new file mode 100644 index 0000000000000000000000000000000000000000..3b80f9966a057a199637726c977aa139c5114429 GIT binary patch literal 193 zcmeAS@N?(olHy`uVBq!ia0vp^0zk~e!3HF=pW8M9DYhhUcNd2LAh=-f^2tCE&H|6f zVxW#|Aj~)+zj6*xkiEpy*OmP~2R9#wqTA#8V?ZH4PZ!4!jq}NO%Ku-lXX9cJW^PS* zz?;y}#werw`QpuppC^P)m>9CX-TnWy{n5CDgO60%)LqR)woO)&KD_VuehDB5cmg6N d+%GdSGt{cYOsoob-2gP1!PC{xWt~$(697@YJX!z% literal 0 HcmV?d00001 diff --git a/www/manager/Makefile b/www/manager/Makefile index 129eca7..eecb64d 100644 --- a/www/manager/Makefile +++ b/www/manager/Makefile @@ -121,6 +121,8 @@ JSSRC= \ qemu/BootOrderEdit.js \ qemu/MemoryEdit.js \ qemu/NetworkEdit.js \ + qemu/IPConfigEdit.js \ + qemu/CloudInitCreator.js \ qemu/Smbios1Edit.js \ qemu/CDEdit.js \ qemu/HDEdit.js \ @@ -134,6 +136,7 @@ JSSRC= \ qemu/StartupEdit.js \ qemu/ScsiHwEdit.js \ qemu/Options.js \ + qemu/CloudInit.js \ qemu/Snapshot.js \ qemu/Clone.js \ qemu/SnapshotTree.js \ diff --git a/www/manager/Parser.js b/www/manager/Parser.js index 5f15a76..fe2cc41 100644 --- a/www/manager/Parser.js +++ b/www/manager/Parser.js @@ -146,6 +146,63 @@ Ext.define('PVE.Parser', { statics: { return drivestr; }, + parseIPConfig: function(key, value) { + if (!(key && value)) { + return; + } + + var res = {}; + + var errors = false; + Ext.Array.each(value.split(','), function(p) { + if (!p || p.match(/^\s*$/)) { + return; // continue + } + + var match_res; + if ((match_res = p.match(/^ip=(\S+)$/)) !== null) { + res.ip = match_res[1]; + } else if ((match_res = p.match(/^gw=(\S+)$/)) !== null) { + res.gw = match_res[1]; + } else if ((match_res = p.match(/^ip6=(\S+)$/)) !== null) { + res.ip6 = match_res[1]; + } else if ((match_res = p.match(/^gw6=(\S+)$/)) !== null) { + res.gw6 = match_res[1]; + } else { + errors = true; + return false; // break + } + }); + + if (errors) { + return; + } + + return res; + }, + + printIPConfig: function(cfg) { + var c = ""; + var str = ""; + if (cfg.ip) { + str += "ip=" + cfg.ip; + c = ","; + } + if (cfg.gw) { + str += c + "gw=" + cfg.gw; + c = ","; + } + if (cfg.ip6) { + str += c + "ip6=" + cfg.ip6; + c = ","; + } + if (cfg.gw6) { + str += c + "gw6=" + cfg.gw6; + c = ","; + } + return str; + }, + parseOpenVZNetIf: function(value) { if (!value) { return; diff --git a/www/manager/Utils.js b/www/manager/Utils.js index 0e2e8a2..adbff79 100644 --- a/www/manager/Utils.js +++ b/www/manager/Utils.js @@ -1083,7 +1083,32 @@ Ext.define('PVE.Utils', { statics: { } PVE.Utils.setErrorMask(me, msg); }); - } + }, + + bus_counts: { ide: 4, sata: 6, scsi: 16, virtio: 16 }, + forEachBus: function(type, func) { + if (Ext.isDefined(type)) { + var count = PVE.Utils.bus_counts[type]; + if (!count) { + Ext.Msg.alert(gettext('Invalid bus type') + ' (' + type + ')'); + return; + } + for (var i = 0; i < count; i++) { + var cont = func(type, i); + if (Ext.isDefined(cont) && cont < 0) + return; + } + } else { + Ext.each(Object.keys(PVE.Utils.bus_counts), function(busname) { + var count = PVE.Utils.bus_counts[busname]; + for (var i = 0; i < count; i++) { + var cont = func(busname, i); + if (Ext.isDefined(cont) && !cont) + return false; + } + }); + } + }, }}); diff --git a/www/manager/qemu/CloudInit.js b/www/manager/qemu/CloudInit.js new file mode 100644 index 0000000..1a4b880 --- /dev/null +++ b/www/manager/qemu/CloudInit.js @@ -0,0 +1,296 @@ +/*jslint confusion: true */ +Ext.define('PVE.qemu.CloudInit', { + extend: 'PVE.grid.PendingObjectGrid', + alias: ['widget.PVE.qemu.CloudInit'], + + initComponent : function() { + var me = this; + var i, confid; + + me.pveBusId = undefined; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var vmid = me.pveSelNode.data.vmid; + if (!vmid) { + throw "no VM ID specified"; + } + + var caps = Ext.state.Manager.get('GuiCap'); + + var rows = {}; + + for (i = 0; i < 32; i++) { + var confid = "ipconfig" + i; + rows[confid] = { + tdCls: 'pve-itype-icon-network', + editor: caps.vms['VM.Config.Network'] ? 'PVE.qemu.IPConfigEdit' : undefined, + never_delete: caps.vms['VM.Config.Network'] ? false : true, + header: gettext('Network') + ' ' + i, + }; + rows["net" + i] = { + never_delete: caps.vms['VM.Config.Network'] ? false : true, + visible: false + }; + } + + // we also need to know whether there's a cloudinit image already + // available + PVE.Utils.forEachBus(undefined, function(type, id) { + rows[type + id] = { visible: false }; + }); + + var reload = function() { + me.rstore.load(); + }; + + var baseurl = 'nodes/' + nodename + '/qemu/' + vmid + '/config'; + + var sm = Ext.create('Ext.selection.RowModel', {}); + + var run_editor = function() { + var rec = sm.getSelection()[0]; + if (!rec) { + return; + } + + var rowdef = rows[rec.data.key]; + if (!rowdef.editor) { + return; + } + + var editor = rowdef.editor; + var win; + + if (Ext.isString(editor)) { + win = Ext.create(editor, { + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }); + } else { + var config = Ext.apply({ + pveSelNode: me.pveSelNode, + confid: rec.data.key, + url: '/api2/extjs/' + baseurl + }, rowdef.editor); + win = Ext.createWidget(rowdef.editor.xtype, config); + win.load(); + } + + win.show(); + win.on('destroy', reload); + }; + + var add_btn = new PVE.button.Button({ + text: gettext('Generate'), + disabled: true, + handler: function() { + var win = Ext.create('PVE.qemu.CloudInitCreator', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode, + pveBusId: me.pveBusId + }); + win.on('destroy', reload); + win.show(); + } + }); + + var delete_btn = new PVE.button.Button({ + text: gettext('Delete'), + disabled: true, + dangerous: true, + confirmMsg: function() { + var msg = gettext('Are you sure you want to remove the CloudInit Image?'); + if (me.pveBusId.match(/^unused\d+$/)) { + msg += " " + gettext('This will permanently erase all image data.'); + } + + return msg; + }, + handler: function(b, e) { + PVE.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'force': 1, + 'delete': me.pveBusId + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }); + + var edit_btn = new PVE.button.Button({ + text: gettext('Edit'), + selModel: sm, + disabled: true, + handler: run_editor + }); + + var remove_btn = new PVE.button.Button({ + text: gettext('Remove'), + selModel: sm, + disabled: true, + dangerous: true, + confirmMsg: function(rec) { + var msg = Ext.String.format(gettext('Are you sure you want to remove entry {0}'), + "'" + me.renderKey(rec.data.key, {}, rec) + "'"); + return msg; + }, + handler: function(b, e, rec) { + PVE.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'delete': rec.data.key + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error', response.htmlStatus); + } + }); + } + }); + + var revert_btn = new PVE.button.Button({ + text: gettext('Revert'), + selModel: sm, + disabled: true, + handler: function(b, e, rec) { + var rowdef = me.rows[rec.data.key] || {}; + var keys = rowdef.multiKey || [ rec.data.key ]; + var revert = keys.join(','); + PVE.Utils.API2Request({ + url: '/api2/extjs/' + baseurl, + waitMsgTarget: me, + method: 'PUT', + params: { + 'revert': revert + }, + callback: function() { + reload(); + }, + failure: function (response, opts) { + Ext.Msg.alert('Error',response.htmlStatus); + } + }); + } + }); + + var set_button_status = function() { + var sm = me.getSelectionModel(); + var rec = sm.getSelection()[0]; + + if (!rec) { + remove_btn.disable(); + edit_btn.disable(); + revert_btn.disable(); + return; + } + var key = rec.data.key; + var value = rec.data.value; + var rowdef = rows[key]; + + var pending = rec.data['delete'] || me.hasPendingChanges(key); + + remove_btn.setDisabled(rec.data['delete'] || (rowdef.never_delete === true)); + + edit_btn.setDisabled(rec.data['delete'] || !rowdef.editor); + + revert_btn.setDisabled(!pending); + }; + + var update_data = function() { + var i; + me.pveBusId = undefined; + PVE.Utils.forEachBus(undefined, function(type, id) { + var confid = type + id; + var entry = me.rstore.getById(confid); + if (!entry) + return; // continue + if (entry.data.value.match(/vm-\d+-cloudinit/)) { + me.pveBusId = confid; + return false; // break + } + }); + if (Ext.isDefined(me.pveBusId)) { + add_btn.setText(gettext('Regenerate')); + add_btn.enable(); + delete_btn.enable(); + } else { + add_btn.setText(gettext('Generate')); + add_btn.enable(); + delete_btn.disable(); + } + + // add/remove arrays because .add/.remove recurses into the + // 'datachange' signal + var to_add = []; + var to_remove = []; + for (i = 0; i < 32; i++) { + var cid = "ipconfig" + i; + var nid = "net" + i; + var dev = me.rstore.getById(nid); + var conf = me.rstore.getById(cid); + if (!dev) { + if (conf) + to_remove.push(conf); + continue; + } + if (!conf) { + to_add.push({ key: cid, value: '' }); + rows[cid].visible = true; + } else + rows[conf.data.key].visible = !!dev; + } + if (to_remove.length) + me.rstore.remove(to_remove); + if (to_add.length) + me.rstore.add(to_add); + }; + + Ext.applyIf(me, { + url: '/api2/json/' + 'nodes/' + nodename + '/qemu/' + vmid + '/pending', + interval: 5000, + selModel: sm, + cwidth1: 170, + tbar: [ + add_btn, + delete_btn, + remove_btn, + edit_btn, + revert_btn + ], + rows: rows, + listeners: { + itemdblclick: run_editor, + selectionchange: set_button_status + } + }); + + me.callParent(); + + me.on('show', me.rstore.startUpdate); + me.on('hide', me.rstore.stopUpdate); + me.on('destroy', me.rstore.stopUpdate); + + me.rstore.on('datachanged', function() { + update_data(); + set_button_status(); + }); + } +}); + diff --git a/www/manager/qemu/CloudInitCreator.js b/www/manager/qemu/CloudInitCreator.js new file mode 100644 index 0000000..757fca0 --- /dev/null +++ b/www/manager/qemu/CloudInitCreator.js @@ -0,0 +1,84 @@ +Ext.define('PVE.qemu.CloudInitCreatePanel', { + extend: 'PVE.panel.InputPanel', + alias: 'widget.PVE.qemu.CloudInitCreatePanel', + + insideWizard: false, + + vmconfig: {}, + + onGetValues: function(values) { + var me = this; + + var confid = me.confid || (values.controller + values.deviceid); + var params = {}; + + params[confid] = values.cdstorage + ":cloudinit"; + + return params; + }, + + setVMConfig: function(vmconfig) { + var me = this; + me.vmconfig = vmconfig; + me.bussel.setVMConfig(vmconfig, true); + }, + + setNodename: function(nodename) { + var me = this; + me.cdstoragesel.setNodename(nodename); + }, + + initComponent : function() { + var me = this; + + me.bussel = Ext.createWidget('PVE.form.ControllerSelector', { + noVirtIO: true + }); + me.cdstoragesel = Ext.create('PVE.form.StorageSelector', { + name: 'cdstorage', + nodename: me.nodename, + fieldLabel: gettext('Storage'), + storageContent: 'images', + autoSelect: true, + allowBlank: false, + }); + + me.column1 = [me.bussel]; + me.column2 = [me.cdstoragesel]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.CloudInitCreator', { + extend: 'PVE.window.Edit', + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + var ipanel = Ext.create('PVE.qemu.CloudInitCreatePanel', { + nodename: nodename + }); + + Ext.applyIf(me, { + subject: gettext('Config Drive'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + ipanel.setVMConfig(me.vmconfig); + } + }); + } +}); diff --git a/www/manager/qemu/Config.js b/www/manager/qemu/Config.js index 19a331b..005f3e9 100644 --- a/www/manager/qemu/Config.js +++ b/www/manager/qemu/Config.js @@ -147,6 +147,11 @@ Ext.define('PVE.qemu.Config', { xtype: 'PVE.qemu.Options' }, { + title: gettext('CloudInit'), + itemId: 'cloudinit', + xtype: 'PVE.qemu.CloudInit' + }, + { title: gettext('Task History'), itemId: 'tasks', xtype: 'pveNodeTasks', diff --git a/www/manager/qemu/HardwareView.js b/www/manager/qemu/HardwareView.js index 89f2f4e..d2b437a 100644 --- a/www/manager/qemu/HardwareView.js +++ b/www/manager/qemu/HardwareView.js @@ -143,8 +143,8 @@ Ext.define('PVE.qemu.HardwareView', { }; - for (i = 0; i < 4; i++) { - confid = "ide" + i; + PVE.Utils.forEachBus(undefined, function(type, id) { + confid = type + id; rows[confid] = { group: 1, tdCls: 'pve-itype-icon-storage', @@ -153,40 +153,7 @@ Ext.define('PVE.qemu.HardwareView', { header: gettext('Hard Disk') + ' (' + confid +')', cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' }; - } - for (i = 0; i < 6; i++) { - confid = "sata" + i; - rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', - editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' - }; - } - for (i = 0; i < 16; i++) { - confid = "scsi" + i; - rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', - editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' - }; - } - for (i = 0; i < 16; i++) { - confid = "virtio" + i; - rows[confid] = { - group: 1, - tdCls: 'pve-itype-icon-storage', - editor: 'PVE.qemu.HDEdit', - never_delete: caps.vms['VM.Config.Disk'] ? false : true, - header: gettext('Hard Disk') + ' (' + confid +')', - cdheader: gettext('CD/DVD Drive') + ' (' + confid +')' - }; - } + }); for (i = 0; i < 32; i++) { confid = "net" + i; rows[confid] = { @@ -504,6 +471,19 @@ Ext.define('PVE.qemu.HardwareView', { win.on('destroy', reload); win.show(); } + }, + { + text: gettext('Config Drive'), + iconCls: 'pve-itype-icon-cloudinit', + disabled: !caps.vms['VM.Config.Network'] || !caps.vms['VM.Config.Disk'], + handler: function() { + var win = Ext.create('PVE.qemu.CloudInitCreator', { + url: '/api2/extjs/' + baseurl, + pveSelNode: me.pveSelNode + }); + win.on('destroy', reload); + win.show(); + } } ] }) diff --git a/www/manager/qemu/IPConfigEdit.js b/www/manager/qemu/IPConfigEdit.js new file mode 100644 index 0000000..8931fae --- /dev/null +++ b/www/manager/qemu/IPConfigEdit.js @@ -0,0 +1,234 @@ +Ext.define('PVE.qemu.IPConfigPanel', { + extend: 'PVE.panel.InputPanel', + alias: 'widget.PVE.qemu.IPConfigPanel', + + insideWizard: false, + + onGetValues: function(values) { + var me = this; + + var data = {}; + + if (values['ipv4mode'] !== 'static') + data['ip'] = values['ipv4mode']; + else { + data['ip'] = values['ip']; + } + + if (values['ipv6mode'] !== 'static') + data['ip6'] = values['ipv6mode']; + else + data['ip6'] = values['ip6']; + + var params = {}; + + if (data['ip'] === '' && data['ip6'] === '' && + data['gw'] === '' && data['gw6'] === '') + { + params['delete'] = [me.confid]; + } else { + params[me.confid] = PVE.Parser.printIPConfig(data); + } + return params; + }, + + setIPConfig: function(confid, data) { + var me = this; + + me.confid = confid; + + if (data['ip'] === 'dhcp') { + data['ipv4mode'] = data['ip']; + data['ip'] = ''; + } else { + data['ipv4mode'] = 'static'; + } + if (data['ip6'] === 'dhcp' || data['ip6'] === 'auto') { + data['ipv6mode'] = data['ip6']; + data['ip6'] = ''; + } else { + data['ipv6mode'] = 'static'; + } + + me.ipconfig = data; + me.setValues(me.ipconfig); + }, + + initComponent : function() { + var me = this; + + me.ipconfig = {}; + me.confid = 'ipconfig0'; + + me.column1 = [ + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + height: 22, // hack: set same height as text fields + items: [ + { + xtype: 'label', + text: gettext('IPv4') + ':', + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv4mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip]').setDisabled(!value); + me.down('field[name=gw]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv4mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip', + vtype: 'IPCIDRAddress', + value: '', + disabled: true, + fieldLabel: gettext('IPv4/CIDR') + }, + { + xtype: 'textfield', + name: 'gw', + value: '', + vtype: 'IPAddress', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv4') +')', + margin: '0 0 3 0' // override bottom margin to account for the menuseparator + }, + ]; + + me.column2 = [ + { + layout: { + type: 'hbox', + align: 'middle' + }, + border: false, + margin: '0 0 5 0', + height: 22, // hack: set same height as text fields + items: [ + { + xtype: 'label', + text: gettext('IPv6') + ':', + }, + { + xtype: 'radiofield', + boxLabel: gettext('Static'), + name: 'ipv6mode', + inputValue: 'static', + checked: false, + margin: '0 0 0 10', + listeners: { + change: function(cb, value) { + me.down('field[name=ip6]').setDisabled(!value); + me.down('field[name=gw6]').setDisabled(!value); + } + } + }, + { + xtype: 'radiofield', + boxLabel: gettext('DHCP'), + name: 'ipv6mode', + inputValue: 'dhcp', + checked: false, + margin: '0 0 0 10' + }, + { + xtype: 'radiofield', + boxLabel: gettext('SLAAC'), + name: 'ipv6mode', + inputValue: 'auto', + checked: false, + margin: '0 0 0 10' + } + ] + }, + { + xtype: 'textfield', + name: 'ip6', + value: '', + vtype: 'IP6CIDRAddress', + disabled: true, + fieldLabel: gettext('IPv6/CIDR') + }, + { + xtype: 'textfield', + name: 'gw6', + vtype: 'IP6Address', + value: '', + disabled: true, + fieldLabel: gettext('Gateway') + ' (' + gettext('IPv6') +')' + } + ]; + + me.callParent(); + } +}); + +Ext.define('PVE.qemu.IPConfigEdit', { + extend: 'PVE.window.Edit', + + isAdd: true, + + initComponent : function() { + /*jslint confusion: true */ + + var me = this; + + var nodename = me.pveSelNode.data.node; + if (!nodename) { + throw "no node name specified"; + } + + me.create = me.confid ? false : true; + + var ipanel = Ext.create('PVE.qemu.IPConfigPanel', { + confid: me.confid, + nodename: nodename + }); + + Ext.applyIf(me, { + subject: gettext('Network Config'), + items: ipanel + }); + + me.callParent(); + + me.load({ + success: function(response, options) { + me.vmconfig = response.result.data; + var ipconfig = {}; + var value = me.vmconfig[me.confid]; + if (value) { + ipconfig = PVE.Parser.parseIPConfig(me.confid, value); + if (!ipconfig) { + Ext.Msg.alert(gettext('Error'), gettext('Unable to parse network configuration')); + me.close(); + return; + } + } + ipanel.setIPConfig(me.confid, ipconfig); + } + }); + } +}); -- 2.1.4 _______________________________________________ pve-devel mailing list pve-devel@pve.proxmox.com http://pve.proxmox.com/cgi-bin/mailman/listinfo/pve-devel