]> git.proxmox.com Git - pve-manager.git/commitdiff
add GUI for openvswitch network
authorDietmar Maurer <dietmar@proxmox.com>
Fri, 27 Dec 2013 08:45:31 +0000 (09:45 +0100)
committerDietmar Maurer <dietmar@proxmox.com>
Fri, 27 Dec 2013 08:45:31 +0000 (09:45 +0100)
PVE/API2/Network.pm
www/manager/Utils.js
www/manager/form/BondModeSelector.js
www/manager/form/BridgeSelector.js
www/manager/node/NetworkEdit.js
www/manager/node/NetworkView.js

index 35b077b97baf6495f9cbb05d5a61cefda6967f33..2a71ffd0b9e4ea3ad3fda6a77af301173e04cca2 100644 (file)
@@ -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);
index 6317b79e01a71b6c1677eba7ec44ad080b551720..88ba93d0f8fe2fce2580dd0eeebf5b39cd1cd9e9 100644 (file)
@@ -77,6 +77,12 @@ Ext.apply(Ext.form.field.VTypes, {
     },
     BondNameText: gettext('Format') + ': bond<b>N</b>, where 0 <= <b>N</b> <= 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)';
index 75a5be814a919f3c3eab56cf2a1063b1d5945dea..4396655ada2cacba7d095a631ec33fd74577c9e1 100644 (file)
@@ -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();
     }
index 1628ef2848ee60c49aca5f8ca8442548b8d7e0f6..0bfd3784efdc1f715efaeaad3edc11f0025a91e5 100644 (file)
@@ -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();
index 374dcd6077142835192464ebb894a9f9dd40bdca..2e071c2baf829b63d5d2af52542588b1d1800404 100644 (file)
@@ -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,
index d8b0ddd7980baa10959d80b6eb08b482a3839472..4b1db77678fd22508318c22f8e927105897602d4 100644 (file)
@@ -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'),