--- /dev/null
+Ext.define('Proxmox.node.NetworkEdit', {
+ extend: 'Proxmox.window.Edit',
+ alias: ['widget.proxmoxNodeNetworkEdit'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ if (!me.iftype) {
+ throw "no network device type specified";
+ }
+
+ me.create = !me.iface;
+
+ var iface_vtype;
+
+ if (me.iftype === 'bridge') {
+ iface_vtype = 'BridgeName';
+ } else if (me.iftype === 'bond') {
+ iface_vtype = 'BondName';
+ } else if (me.iftype === 'eth' && !me.create) {
+ iface_vtype = 'InterfaceName';
+ } else if (me.iftype === 'vlan' && !me.create) {
+ 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 {
+ console.log(me.iftype);
+ throw "unknown network device type specified";
+ }
+
+ me.subject = Proxmox.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({
+ xtype: 'pvecheckbox',
+ fieldLabel: gettext('VLAN aware'),
+ name: 'bridge_vlan_aware',
+ deleteEmpty: !me.create
+ });
+ column2.push({
+ xtype: 'textfield',
+ 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',
+ fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+ allowBlank: false,
+ nodename: me.nodename,
+ bridgeType: 'OVSBridge',
+ name: 'ovs_bridge'
+ });
+ column2.push({
+ xtype: 'pveVlanField',
+ deleteEmpty: !me.create,
+ name: 'ovs_tag',
+ value: ''
+ });
+ column2.push({
+ xtype: 'textfield',
+ fieldLabel: gettext('OVS options'),
+ name: 'ovs_options'
+ });
+ } else if (me.iftype === 'bond') {
+ column2.push({
+ xtype: 'textfield',
+ fieldLabel: gettext('Slaves'),
+ name: 'slaves'
+ });
+
+ var policySelector = Ext.createWidget('bondPolicySelector', {
+ fieldLabel: gettext('Hash policy'),
+ name: 'bond_xmit_hash_policy',
+ deleteEmpty: !me.create,
+ disabled: true
+ });
+
+ column2.push({
+ xtype: 'bondModeSelector',
+ fieldLabel: gettext('Mode'),
+ name: 'bond_mode',
+ value: me.create ? 'balance-rr' : undefined,
+ listeners: {
+ change: function(f, value) {
+ if (value === 'balance-xor' ||
+ value === '802.3ad') {
+ policySelector.setDisabled(false);
+ } else {
+ policySelector.setDisabled(true);
+ policySelector.setValue('');
+ }
+ }
+ },
+ allowBlank: false
+ });
+
+ column2.push(policySelector);
+
+ } else if (me.iftype === 'OVSBond') {
+ column2.push({
+ xtype: me.create ? 'PVE.form.BridgeSelector' : 'displayfield',
+ fieldLabel: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+ allowBlank: false,
+ nodename: me.nodename,
+ bridgeType: 'OVSBridge',
+ name: 'ovs_bridge'
+ });
+ column2.push({
+ xtype: 'pveVlanField',
+ deleteEmpty: !me.create,
+ name: 'ovs_tag',
+ value: ''
+ });
+ column2.push({
+ xtype: 'textfield',
+ fieldLabel: gettext('OVS options'),
+ name: 'ovs_options'
+ });
+ }
+
+ column2.push({
+ xtype: 'textfield',
+ fieldLabel: gettext('Comment'),
+ allowBlank: true,
+ nodename: me.nodename,
+ name: 'comments'
+ });
+
+ var url;
+ var method;
+
+ if (me.create) {
+ url = "/api2/extjs/nodes/" + me.nodename + "/network";
+ method = 'POST';
+ } else {
+ url = "/api2/extjs/nodes/" + me.nodename + "/network/" + me.iface;
+ method = 'PUT';
+ }
+
+ var column1 = [
+ {
+ xtype: 'hiddenfield',
+ name: 'type',
+ value: me.iftype
+ },
+ {
+ xtype: me.create ? 'textfield' : 'displayfield',
+ fieldLabel: gettext('Name'),
+ name: 'iface',
+ value: me.iface,
+ vtype: iface_vtype,
+ allowBlank: false
+ }
+ ];
+
+ if (me.iftype === 'OVSBond') {
+ column1.push(
+ {
+ xtype: 'bondModeSelector',
+ fieldLabel: gettext('Mode'),
+ name: 'bond_mode',
+ openvswitch: true,
+ value: me.create ? 'active-backup' : undefined,
+ allowBlank: false
+ },
+ {
+ xtype: 'textfield',
+ fieldLabel: gettext('Slaves'),
+ name: 'ovs_bonds'
+ }
+ );
+ } 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;
+ }
+ 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;
+ }
+ },
+ {
+ xtype: 'pvetextfield',
+ deleteEmpty: !me.create,
+ fieldLabel: gettext('Gateway'),
+ vtype: 'IPAddress',
+ name: 'gateway'
+ },
+ {
+ xtype: 'pvetextfield',
+ deleteEmpty: !me.create,
+ fieldLabel: gettext('IPv6 address'),
+ vtype: 'IP6Address',
+ name: 'address6'
+ },
+ {
+ xtype: 'pvetextfield',
+ deleteEmpty: !me.create,
+ fieldLabel: gettext('Prefix length'),
+ vtype: 'IP6PrefixLength',
+ name: 'netmask6',
+ value: '',
+ allowBlank: true,
+ validator: function(value) {
+ /*jslint confusion: true */
+ if (!me.items) {
+ return true;
+ }
+ var address = me.down('field[name=address6]').getValue();
+ if (value !== '') {
+ if (address === '') {
+ return "IPv6 prefix length requires option 'IPv6 address'";
+ }
+ } else {
+ if (address !== '') {
+ return "Option 'IPv6 address' requires an IPv6 prefix length";
+ }
+ }
+
+ return true;
+ }
+ },
+ {
+ xtype: 'pvetextfield',
+ deleteEmpty: !me.create,
+ fieldLabel: gettext('Gateway'),
+ vtype: 'IP6Address',
+ name: 'gateway6'
+ }
+ );
+ }
+
+ Ext.applyIf(me, {
+ url: url,
+ method: method,
+ items: {
+ xtype: 'inputpanel',
+ column1: column1,
+ column2: column2
+ }
+ });
+
+ me.callParent();
+
+ if (me.create) {
+ me.down('field[name=iface]').setValue(me.iface_default);
+ } else {
+ me.load({
+ success: function(response, options) {
+ var data = response.result.data;
+ if (data.type !== me.iftype) {
+ var msg = "Got unexpected device type";
+ Ext.Msg.alert(gettext('Error'), msg, function() {
+ me.close();
+ });
+ return;
+ }
+ me.setValues(data);
+ me.isValid(); // trigger validation
+ }
+ });
+ }
+ }
+});
--- /dev/null
+Ext.define('proxmox-networks', {
+ extend: 'Ext.data.Model',
+ fields: [
+ 'iface', 'type', 'active', 'autostart',
+ 'bridge_ports', 'slaves',
+ 'address', 'netmask', 'gateway',
+ 'address6', 'netmask6', 'gateway6',
+ 'comments'
+ ],
+ idProperty: 'iface'
+});
+
+Ext.define('Proxmox.node.NetworkView', {
+ extend: 'Ext.panel.Panel',
+
+ alias: ['widget.proxmoxNodeNetworkView'],
+
+ initComponent : function() {
+ var me = this;
+
+ if (!me.nodename) {
+ throw "no node name specified";
+ }
+
+ var baseUrl = '/nodes/' + me.nodename + '/network';
+
+ var store = Ext.create('Ext.data.Store', {
+ model: 'proxmox-networks',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json' + baseUrl
+ },
+ sorters: [
+ {
+ property : 'iface',
+ direction: 'ASC'
+ }
+ ]
+ });
+
+ var reload = function() {
+ var changeitem = me.down('#changes');
+ Proxmox.Utils.API2Request({
+ url: baseUrl,
+ failure: function(response, opts) {
+ changeitem.update(gettext('Error') + ': ' + response.htmlStatus);
+ store.loadData({});
+ },
+ success: function(response, opts) {
+ var result = Ext.decode(response.responseText);
+ store.loadData(result.data);
+ var changes = result.changes;
+ if (changes === undefined || changes === '') {
+ changes = gettext("No changes");
+ }
+ changeitem.update("<pre>" + Ext.htmlEncode(changes) + "</pre>");
+ }
+ });
+ };
+
+ var run_editor = function() {
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iface: rec.data.iface,
+ iftype: rec.data.type
+ });
+ win.show();
+ win.on('destroy', reload);
+ };
+
+ var edit_btn = new Ext.Button({
+ text: gettext('Edit'),
+ disabled: true,
+ handler: run_editor
+ });
+
+ var del_btn = new Ext.Button({
+ text: gettext('Remove'),
+ disabled: true,
+ handler: function(){
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+ if (!rec) {
+ return;
+ }
+
+ var iface = rec.data.iface;
+
+ Proxmox.Utils.API2Request({
+ url: baseUrl + '/' + iface,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ });
+
+ var set_button_status = function() {
+ var grid = me.down('gridpanel');
+ var sm = grid.getSelectionModel();
+ var rec = sm.getSelection()[0];
+
+ edit_btn.setDisabled(!rec);
+ del_btn.setDisabled(!rec);
+ };
+
+ Proxmox.Utils.monStoreErrors(me, store);
+
+ var render_ports = function(value, metaData, record) {
+ if (value === 'bridge') {
+ 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: Proxmox.Utils.render_network_iface_type('bridge'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'bridge',
+ iface_default: find_next_iface_id('vmbr')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('bond'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'bond',
+ iface_default: find_next_iface_id('bond')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ }, '-',
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSBridge'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSBridge',
+ iface_default: find_next_iface_id('vmbr')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSBond'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSBond',
+ iface_default: find_next_iface_id('bond')
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ },
+ {
+ text: Proxmox.Utils.render_network_iface_type('OVSIntPort'),
+ handler: function() {
+ var win = Ext.create('Proxmox.node.NetworkEdit', {
+ nodename: me.nodename,
+ iftype: 'OVSIntPort'
+ });
+ win.on('destroy', reload);
+ win.show();
+ }
+ }
+ ]
+ })
+ }, ' ',
+ {
+ text: gettext('Revert'),
+ handler: function() {
+ Proxmox.Utils.API2Request({
+ url: baseUrl,
+ method: 'DELETE',
+ waitMsgTarget: me,
+ callback: function() {
+ reload();
+ },
+ failure: function(response, opts) {
+ Ext.Msg.alert(gettext('Error'), response.htmlStatus);
+ }
+ });
+ }
+ },
+ edit_btn,
+ del_btn
+ ],
+ items: [
+ {
+ xtype: 'gridpanel',
+ stateful: true,
+ stateId: 'grid-node-network',
+ store: store,
+ region: 'center',
+ border: false,
+ columns: [
+ {
+ header: gettext('Name'),
+ width: 100,
+ sortable: true,
+ dataIndex: 'iface'
+ },
+ {
+ header: gettext('Type'),
+ width: 100,
+ sortable: true,
+ renderer: Proxmox.Utils.render_network_iface_type,
+ dataIndex: 'type'
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('Active'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'active',
+ trueText: 'Yes',
+ falseText: 'No',
+ undefinedText: 'No'
+ },
+ {
+ xtype: 'booleancolumn',
+ header: gettext('Autostart'),
+ width: 80,
+ sortable: true,
+ dataIndex: 'autostart',
+ trueText: 'Yes',
+ falseText: 'No',
+ undefinedText: 'No'
+ },
+ {
+ header: gettext('Ports/Slaves'),
+ dataIndex: 'type',
+ renderer: render_ports
+ },
+ {
+ header: gettext('IP address'),
+ sortable: true,
+ dataIndex: 'address',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.address && rec.data.address6) {
+ return rec.data.address + "<br>"
+ + rec.data.address6 + '/' + rec.data.netmask6;
+ } else if (rec.data.address6) {
+ return rec.data.address6 + '/' + rec.data.netmask6;
+ } else {
+ return rec.data.address;
+ }
+ }
+ },
+ {
+ header: gettext('Subnet mask'),
+ sortable: true,
+ dataIndex: 'netmask'
+ },
+ {
+ header: gettext('Gateway'),
+ sortable: true,
+ dataIndex: 'gateway',
+ renderer: function(value, metaData, rec) {
+ if (rec.data.gateway && rec.data.gateway6) {
+ return rec.data.gateway + "<br>" + rec.data.gateway6;
+ } else if (rec.data.gateway6) {
+ return rec.data.gateway6;
+ } else {
+ return rec.data.gateway;
+ }
+ }
+ },
+ {
+ header: gettext('Comment'),
+ dataIndex: 'comments',
+ renderer: Ext.String.htmlEncode
+ }
+ ],
+ listeners: {
+ selectionchange: set_button_status,
+ itemdblclick: run_editor
+ }
+ },
+ {
+ border: false,
+ region: 'south',
+ autoScroll: true,
+ itemId: 'changes',
+ tbar: [
+ gettext('Pending changes') + ' (' +
+ gettext('Please reboot to activate changes') + ')'
+ ],
+ split: true,
+ bodyPadding: 5,
+ flex: 0.6,
+ html: gettext("No changes")
+ }
+ ],
+ listeners: {
+ activate: reload
+ }
+ });
+
+ me.callParent();
+ }
+});