From d11733f8da7ba626a8861f49fd4389a618025ae1 Mon Sep 17 00:00:00 2001 From: Dietmar Maurer Date: Fri, 27 Dec 2013 09:45:31 +0100 Subject: [PATCH] add GUI for openvswitch network --- PVE/API2/Network.pm | 61 +++++++++- www/manager/Utils.js | 21 ++++ www/manager/form/BondModeSelector.js | 28 +++-- www/manager/form/BridgeSelector.js | 5 +- www/manager/node/NetworkEdit.js | 172 ++++++++++++++++++++------- www/manager/node/NetworkView.js | 80 ++++++++++--- 6 files changed, 292 insertions(+), 75 deletions(-) diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm index 35b077b9..2a71ffd0 100644 --- a/PVE/API2/Network.pm +++ b/PVE/API2/Network.pm @@ -25,10 +25,20 @@ my $bond_mode_enum = [ 'broadcast', '802.3ad', 'balance-tlb', - 'balance-alb' + 'balance-alb', + 'balance-slb', # OVS only + 'balance-tcp', # OVS only ]; +my $network_type_enum = ['bridge', 'bond', 'eth', 'alias', + 'OVSBridge', 'OVSBond', 'OVSPort', 'OVSIntPort']; + my $confdesc = { + type => { + description => "Network interface type", + type => 'string', + enum => [@$network_type_enum, 'unknown'], + }, autostart => { description => "Automatically start interface on boot.", type => 'boolean', @@ -39,11 +49,32 @@ my $confdesc = { optional => 1, type => 'string', format => 'pve-iface-list', }, + ovs_ports => { + description => "Specify the iterfaces you want to add to your bridge.", + optional => 1, + type => 'string', format => 'pve-iface-list', + }, + ovs_options => { + description => "OVS interface options.", + optional => 1, + type => 'string', + maxLength => 1024, + }, + ovs_bridge => { + description => "The OVS bridge associated with a OVS port. This is required when you create an OVS port.", + optional => 1, + type => 'string', format => 'pve-iface', + }, slaves => { description => "Specify the interfaces used by the bonding device.", optional => 1, type => 'string', format => 'pve-iface-list', }, + ovs_bonds => { + description => "Specify the interfaces used by the bonding device.", + optional => 1, + type => 'string', format => 'pve-iface-list', + }, bond_mode => { description => "Bonding mode.", optional => 1, @@ -92,7 +123,7 @@ __PACKAGE__->register_method({ type => { description => "Only list specific interface types.", type => 'string', - enum => ['bond', 'bridge', 'alias', 'eth'], + enum => $network_type_enum, optional => 1, }, }, @@ -210,6 +241,19 @@ __PACKAGE__->register_method({ $param->{method} = $param->{address} ? 'static' : 'manual'; + if ($param->{type} eq 'OVSIntPort' || $param->{type} eq 'OVSBond') { + my $brname = $param->{ovs_bridge}; + raise_param_exc({ ovs_bridge => "parameter is required" }) if !$brname; + my $br = $config->{$brname}; + raise_param_exc({ ovs_bridge => "bridge '$brname' does not exist" }) if !$br; + raise_param_exc({ ovs_bridge => "interface '$brname' is no OVS bridge" }) + if $br->{type} ne 'OVSBridge'; + + my @ports = split (/\s+/, $br->{ovs_ports} || ''); + $br->{ovs_ports} = join(' ', @ports, $iface) + if ! grep { $_ eq $iface } @ports; + } + $config->{$iface} = $param; PVE::INotify::write_file('interfaces', $config); @@ -346,6 +390,19 @@ __PACKAGE__->register_method({ raise_param_exc({ iface => "interface does not exist" }) if !$config->{$param->{iface}}; + my $d = $config->{$param->{iface}}; + if ($d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') { + if (my $brname = $d->{ovs_bridge}) { + if (my $br = $config->{$brname}) { + if ($br->{ovs_ports}) { + my @ports = split (/\s+/, $br->{ovs_ports}); + my @new = grep { $_ ne $param->{iface} } @ports; + $br->{ovs_ports} = join(' ', @new); + } + } + } + } + delete $config->{$param->{iface}}; PVE::INotify::write_file('interfaces', $config); diff --git a/www/manager/Utils.js b/www/manager/Utils.js index 6317b79e..88ba93d0 100644 --- a/www/manager/Utils.js +++ b/www/manager/Utils.js @@ -77,6 +77,12 @@ Ext.apply(Ext.form.field.VTypes, { }, BondNameText: gettext('Format') + ': bondN, where 0 <= N <= 9999', + InterfaceName: function(v) { + return (/^[a-z][a-z0-9_]{1,20}$/).test(v); + }, + InterfaceNameText: gettext('Format') + ': [a-z][a-z0-9_]{1,20}', + + QemuStartDate: function(v) { return (/^(now|\d{4}-\d{1,2}-\d{1,2}(T\d{1,2}:\d{1,2}:\d{1,2})?)$/).test(v); }, @@ -160,6 +166,21 @@ Ext.define('PVE.Utils', { statics: { return value; }, + network_iface_types: { + eth: gettext("Network Device"), + bridge: 'Linux Bridge', + bond: 'Linux Bond', + OVSBridge: 'OVS Bridge', + OVSBond: 'OVS Bond', + OVSPort: 'OVS Port', + OVSIntPort: 'OVS IntPort' + }, + + render_network_iface_type: function(value) { + return PVE.Utils.network_iface_types[value] || + PVE.Utils.unknownText; + }, + render_scsihw: function(value) { if (!value) { return PVE.Utils.defaultText + ' (LSI 53C895A)'; diff --git a/www/manager/form/BondModeSelector.js b/www/manager/form/BondModeSelector.js index 75a5be81..4396655a 100644 --- a/www/manager/form/BondModeSelector.js +++ b/www/manager/form/BondModeSelector.js @@ -2,18 +2,28 @@ Ext.define('PVE.form.BondModeSelector', { extend: 'PVE.form.KVComboBox', alias: ['widget.bondModeSelector'], + openvswitch: false, + initComponent: function() { var me = this; - me.data = [ - ['balance-rr', 'balance-rr'], - ['active-backup', 'active-backup'], - ['balance-xor', 'balance-xor'], - ['broadcast', 'broadcast'], - ['802.3ad', '802.3ad'], - ['balance-tlb', 'balance-tlb'], - ['balance-alb', 'balance-alb'] - ]; + if (me.openvswitch) { + me.data = [ + ['balance-tcp', 'balance-tcp'], + ['balance-slb', 'balance-slb'], + ['active-backup', 'active-backup'] + ]; + } else { + me.data = [ + ['balance-rr', 'balance-rr'], + ['active-backup', 'active-backup'], + ['balance-xor', 'balance-xor'], + ['broadcast', 'broadcast'], + ['802.3ad', '802.3ad'], + ['balance-tlb', 'balance-tlb'], + ['balance-alb', 'balance-alb'] + ]; + } me.callParent(); } diff --git a/www/manager/form/BridgeSelector.js b/www/manager/form/BridgeSelector.js index 1628ef28..0bfd3784 100644 --- a/www/manager/form/BridgeSelector.js +++ b/www/manager/form/BridgeSelector.js @@ -2,6 +2,8 @@ Ext.define('PVE.form.BridgeSelector', { extend: 'PVE.form.ComboGrid', alias: ['widget.PVE.form.BridgeSelector'], + bridgeType: 'bridge', // or OVSBridge + setNodename: function(nodename) { var me = this; @@ -13,7 +15,8 @@ Ext.define('PVE.form.BridgeSelector', { me.store.setProxy({ type: 'pve', - url: '/api2/json/nodes/' + me.nodename + '/network?type=bridge' + url: '/api2/json/nodes/' + me.nodename + '/network?type=' + + me.bridgeType }); me.store.load(); diff --git a/www/manager/node/NetworkEdit.js b/www/manager/node/NetworkEdit.js index 374dcd60..2e071c2b 100644 --- a/www/manager/node/NetworkEdit.js +++ b/www/manager/node/NetworkEdit.js @@ -19,26 +19,38 @@ Ext.define('PVE.node.NetworkEdit', { var iface_vtype; if (me.iftype === 'bridge') { - me.subject = "Bridge"; iface_vtype = 'BridgeName'; } else if (me.iftype === 'bond') { - me.subject = "Bond"; iface_vtype = 'BondName'; } else if (me.iftype === 'eth' && !me.create) { - me.subject = gettext("Network Device"); + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'OVSBridge') { + iface_vtype = 'BridgeName'; + } else if (me.iftype === 'OVSBond') { + iface_vtype = 'BondName'; + } else if (me.iftype === 'OVSIntPort') { + iface_vtype = 'InterfaceName'; + } else if (me.iftype === 'OVSPort') { + iface_vtype = 'InterfaceName'; } else { - throw "no known network device type specified"; + console.log(me.iftype); + throw "unknown network device type specified"; } - var column2 = [ - { + me.subject = PVE.Utils.render_network_iface_type(me.iftype); + + var column2 = []; + + if (!(me.iftype === 'OVSIntPort' || me.iftype === 'OVSPort' || + me.iftype === 'OVSBond')) { + column2.push({ xtype: 'pvecheckbox', fieldLabel: gettext('Autostart'), name: 'autostart', uncheckedValue: 0, checked: me.create ? true : undefined - } - ]; + }); + } if (me.iftype === 'bridge') { column2.push({ @@ -46,6 +58,32 @@ Ext.define('PVE.node.NetworkEdit', { fieldLabel: gettext('Bridge ports'), name: 'bridge_ports' }); + } else if (me.iftype === 'OVSBridge') { + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('Bridge ports'), + name: 'ovs_ports' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); + } else if (me.iftype === 'OVSPort' || me.iftype === 'OVSIntPort') { + column2.push({ + xtype: me.create ? 'PVE.form.BridgeSelector' : 'displayfield', + height: 22, // hack: set same height as text fields + fieldLabel: PVE.Utils.render_network_iface_type('OVSBridge'), + allowBlank: false, + nodename: nodename, + bridgeType: 'OVSBridge', + name: 'ovs_bridge' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); } else if (me.iftype === 'bond') { column2.push({ xtype: 'textfield', @@ -59,6 +97,21 @@ Ext.define('PVE.node.NetworkEdit', { value: me.create ? 'balance-rr' : undefined, allowBlank: false }); + } else if (me.iftype === 'OVSBond') { + column2.push({ + xtype: me.create ? 'PVE.form.BridgeSelector' : 'displayfield', + height: 22, // hack: set same height as text fields + fieldLabel: PVE.Utils.render_network_iface_type('OVSBridge'), + allowBlank: false, + nodename: nodename, + bridgeType: 'OVSBridge', + name: 'ovs_bridge' + }); + column2.push({ + xtype: 'textfield', + fieldLabel: gettext('OVS options'), + name: 'ovs_options' + }); } var url; @@ -73,6 +126,11 @@ Ext.define('PVE.node.NetworkEdit', { } var column1 = [ + { + xtype: 'hiddenfield', + name: 'type', + value: me.iftype + }, { xtype: me.create ? 'textfield' : 'displayfield', fieldLabel: gettext('Name'), @@ -81,47 +139,71 @@ Ext.define('PVE.node.NetworkEdit', { value: me.iface, vtype: iface_vtype, allowBlank: false - }, - { - xtype: 'pvetextfield', - deleteEmpty: !me.create, - fieldLabel: gettext('IP address'), - vtype: 'IPAddress', - name: 'address' - }, - { - xtype: 'pvetextfield', - deleteEmpty: !me.create, - fieldLabel: gettext('Subnet mask'), - vtype: 'IPAddress', - name: 'netmask', - validator: function(value) { - /*jslint confusion: true */ - if (!me.items) { - return true; - } - var address = me.down('field[name=address]').getValue(); - if (value !== '') { - if (address === '') { - return "Subnet mask requires option 'IP address'"; + } + ]; + + if (me.iftype === 'OVSPort') { + // nothing to edit + } else if (me.iftype === 'OVSBond') { + column1.push([ + { + xtype: 'textfield', + fieldLabel: gettext('Slaves'), + name: 'ovs_bonds' + }, + { + xtype: 'bondModeSelector', + fieldLabel: gettext('Mode'), + name: 'bond_mode', + openvswitch: true, + value: me.create ? 'active-backup' : undefined, + allowBlank: false + } + ]); + } else { + + column1.push([ + { + xtype: 'pvetextfield', + deleteEmpty: !me.create, + fieldLabel: gettext('IP address'), + vtype: 'IPAddress', + name: 'address' + }, + { + xtype: 'pvetextfield', + deleteEmpty: !me.create, + fieldLabel: gettext('Subnet mask'), + vtype: 'IPAddress', + name: 'netmask', + validator: function(value) { + /*jslint confusion: true */ + if (!me.items) { + return true; } - } else { - if (address !== '') { - return "Option 'IP address' requires a subnet mask"; + var address = me.down('field[name=address]').getValue(); + if (value !== '') { + if (address === '') { + return "Subnet mask requires option 'IP address'"; + } + } else { + if (address !== '') { + return "Option 'IP address' requires a subnet mask"; + } } - } - return true; + return true; + } + }, + { + xtype: 'pvetextfield', + deleteEmpty: !me.create, + fieldLabel: gettext('Gateway'), + vtype: 'IPAddress', + name: 'gateway' } - }, - { - xtype: 'pvetextfield', - deleteEmpty: !me.create, - fieldLabel: gettext('Gateway'), - vtype: 'IPAddress', - name: 'gateway' - } - ]; + ]); + } Ext.applyIf(me, { url: url, diff --git a/www/manager/node/NetworkView.js b/www/manager/node/NetworkView.js index d8b0ddd7..4b1db776 100644 --- a/www/manager/node/NetworkView.js +++ b/www/manager/node/NetworkView.js @@ -111,53 +111,90 @@ Ext.define('PVE.node.NetworkView', { return record.data.bridge_ports; } else if (value === 'bond') { return record.data.slaves; + } else if (value === 'OVSBridge') { + return record.data.ovs_ports; + } else if (value === 'OVSBond') { + return record.data.ovs_bonds; } }; + var find_next_iface_id = function(prefix) { + var next; + for (next = 0; next <= 9999; next++) { + if (!store.getById(prefix + next.toString())) { + break; + } + } + return prefix + next.toString(); + }; + Ext.apply(me, { layout: 'border', tbar: [ { text: gettext('Create'), menu: new Ext.menu.Menu({ + plain: true, items: [ { - text: 'Bridge', + text: PVE.Utils.render_network_iface_type('bridge'), handler: function() { - var next; - for (next = 0; next <= 9999; next++) { - if (!store.data.get('vmbr' + next.toString())) { - break; - } - } - var win = Ext.create('PVE.node.NetworkEdit', { pveSelNode: me.pveSelNode, iftype: 'bridge', - iface_default: 'vmbr' + next.toString() + iface_default: find_next_iface_id('vmbr') }); win.on('destroy', reload); win.show(); } }, { - text: 'Bond', + text: PVE.Utils.render_network_iface_type('bond'), handler: function() { - var next; - for (next = 0; next <= 9999; next++) { - if (!store.data.get('bond' + next.toString())) { - break; - } - } var win = Ext.create('PVE.node.NetworkEdit', { pveSelNode: me.pveSelNode, iftype: 'bond', - iface_default: 'bond' + next.toString() + iface_default: find_next_iface_id('bond') + }); + win.on('destroy', reload); + win.show(); + } + }, '-', + { + text: PVE.Utils.render_network_iface_type('OVSBridge'), + handler: function() { + var win = Ext.create('PVE.node.NetworkEdit', { + pveSelNode: me.pveSelNode, + iftype: 'OVSBridge', + iface_default: find_next_iface_id('vmbr') }); win.on('destroy', reload); win.show(); } - } + }, + { + text: PVE.Utils.render_network_iface_type('OVSBond'), + handler: function() { + var win = Ext.create('PVE.node.NetworkEdit', { + pveSelNode: me.pveSelNode, + iftype: 'OVSBond', + iface_default: find_next_iface_id('bond') + }); + win.on('destroy', reload); + win.show(); + } + }, + { + text: PVE.Utils.render_network_iface_type('OVSIntPort'), + handler: function() { + var win = Ext.create('PVE.node.NetworkEdit', { + pveSelNode: me.pveSelNode, + iftype: 'OVSIntPort' + }); + win.on('destroy', reload); + win.show(); + } + } ] }) }, ' ', @@ -194,6 +231,13 @@ Ext.define('PVE.node.NetworkView', { sortable: true, dataIndex: 'iface' }, + { + header: gettext('Type'), + width: 100, + sortable: true, + renderer: PVE.Utils.render_network_iface_type, + dataIndex: 'type' + }, { xtype: 'booleancolumn', header: gettext('Active'), -- 2.39.2