[pve-devel] [PATCH v4 pve-manager 20/33] sdn: ipam: add ipam panel

Dominik Csapak d.csapak at proxmox.com
Mon Nov 20 14:44:43 CET 2023


generally looks ok, but i couldn't edit a manual dhcp
mapping with the error:

vmid: type check ('integer') failed - got ''

did not look too deep so i'm not sure where the empty vmid value
is coming from


also i like alexandres idea to put it into the main tree in the zone
panel as new panel that way we could even reduce the tree by one level
since we don't have to add the zone here

one minor thing inline (but that's not a blocker for me)

On 11/17/23 12:39, Stefan Hanreich wrote:
> Signed-off-by: Stefan Hanreich <s.hanreich at proxmox.com>
> ---
>   www/css/ext6-pve.css          |  22 ++-
>   www/manager6/Makefile         |   1 +
>   www/manager6/dc/Config.js     |  12 +-
>   www/manager6/sdn/IpamEdit.js  |  78 ++++++++++
>   www/manager6/tree/DhcpTree.js | 267 ++++++++++++++++++++++++++++++++++
>   5 files changed, 372 insertions(+), 8 deletions(-)
>   create mode 100644 www/manager6/sdn/IpamEdit.js
>   create mode 100644 www/manager6/tree/DhcpTree.js
> 
> diff --git a/www/css/ext6-pve.css b/www/css/ext6-pve.css
> index e18b173f5..091855356 100644
> --- a/www/css/ext6-pve.css
> +++ b/www/css/ext6-pve.css
> @@ -510,28 +510,38 @@ div.right-aligned {
>       content: ' ';
>   }
>   
> -.fa-sdn:before {
> +.x-fa-sdn-treelist:before {
>       width: 14px;
>       height: 14px;
>       position: absolute;
>       left: 1px;
>       top: 4px;
> +}
> +
> +.fa-sdn:before {
>       background-image:url(../images/icon-sdn.svg);
>       background-size: 14px 14px;
>       content: ' ';
>   }
>   
>   .fa-network-wired:before {
> -    width: 14px;
> -    height: 14px;
> -    position: absolute;
> -    left: 1px;
> -    top: 4px;
>       background-image:url(../images/icon-fa-network-wired.svg);
>       background-size: 14px 14px;
>       content: ' ';
>   }
>   
> +.x-fa-treepanel:before {
> +    width: 16px;
> +    height: 24px;
> +    display: block;
> +    background-repeat: no-repeat;
> +    background-position: center;
> +}
> +
> +.x-tree-icon-none {
> +    display: none;
> +}
> +
>   .x-treelist-row-over > * > .x-treelist-item-icon,
>   .x-treelist-row-over > * > .x-treelist-item-text{
>       color: #000;
> diff --git a/www/manager6/Makefile b/www/manager6/Makefile
> index 093452cd7..93b4ff155 100644
> --- a/www/manager6/Makefile
> +++ b/www/manager6/Makefile
> @@ -108,6 +108,7 @@ JSSRC= 							\
>   	tree/ResourceTree.js				\
>   	tree/SnapshotTree.js				\
>   	tree/ResourceMapTree.js				\
> +	tree/DhcpTree.js				\
>   	window/Backup.js				\
>   	window/BackupConfig.js				\
>   	window/BulkAction.js				\
> diff --git a/www/manager6/dc/Config.js b/www/manager6/dc/Config.js
> index 7d01da5fb..7c2b7b168 100644
> --- a/www/manager6/dc/Config.js
> +++ b/www/manager6/dc/Config.js
> @@ -185,7 +185,7 @@ Ext.define('PVE.dc.Config', {
>   		me.items.push({
>   		    xtype: 'pveSDNStatus',
>   		    title: gettext('SDN'),
> -		    iconCls: 'fa fa-sdn',
> +		    iconCls: 'fa fa-sdn x-fa-sdn-treelist',
>   		    hidden: true,
>   		    itemId: 'sdn',
>   		    expandedOnInit: true,
> @@ -203,7 +203,7 @@ Ext.define('PVE.dc.Config', {
>   		    groups: ['sdn'],
>   		    title: 'VNets',
>   		    hidden: true,
> -		    iconCls: 'fa fa-network-wired',
> +		    iconCls: 'fa fa-network-wired x-fa-sdn-treelist',
>   		    itemId: 'sdnvnet',
>   		},
>   		{
> @@ -213,6 +213,14 @@ Ext.define('PVE.dc.Config', {
>   		    hidden: true,
>   		    iconCls: 'fa fa-gear',
>   		    itemId: 'sdnoptions',
> +		},
> +		{
> +		    xtype: 'pveDhcpTree',
> +		    groups: ['sdn'],
> +		    title: gettext('IPAM'),
> +		    hidden: true,
> +		    iconCls: 'fa fa-map-signs',
> +		    itemId: 'sdnmappings',
>   		});
>   	    }
>   
> diff --git a/www/manager6/sdn/IpamEdit.js b/www/manager6/sdn/IpamEdit.js
> new file mode 100644
> index 000000000..18e22c592
> --- /dev/null
> +++ b/www/manager6/sdn/IpamEdit.js
> @@ -0,0 +1,78 @@
> +Ext.define('PVE.sdn.IpamEditInputPanel', {
> +    extend: 'Proxmox.panel.InputPanel',
> +    mixins: ['Proxmox.Mixin.CBind'],
> +
> +    isCreate: false,
> +
> +    onGetValues: function(values) {
> +	let me = this;
> +
> +	if (!values.vmid) {
> +	    delete values.vmid;
> +	}
> +
> +	return values;
> +    },
> +
> +    items: [
> +	{
> +	    xtype: 'pmxDisplayEditField',
> +	    name: 'vmid',
> +	    fieldLabel: gettext('VMID'),
> +	    allowBlank: false,
> +	    editable: false,
> +	    cbind: {
> +		hidden: '{isCreate}',
> +	    },
> +	},
> +	{
> +	    xtype: 'pmxDisplayEditField',
> +	    name: 'mac',
> +	    fieldLabel: gettext('MAC'),
> +	    allowBlank: false,
> +	    cbind: {
> +		editable: '{isCreate}',
> +	    },
> +	},
> +	{
> +	    xtype: 'proxmoxtextfield',
> +	    name: 'ip',
> +	    fieldLabel: gettext('IP'),
> +	    allowBlank: false,
> +	},
> +    ],
> +});
> +
> +Ext.define('PVE.sdn.IpamEdit', {
> +    extend: 'Proxmox.window.Edit',
> +
> +    subject: gettext('DHCP Mapping'),
> +    width: 350,
> +
> +    isCreate: false,
> +    mapping: {},
> +
> +    submitUrl: function(url, values) {
> +	return `${url}/${values.zone}/${values.vnet}/${values.mac}`;
> +    },
> +
> +    initComponent: function() {
> +	var me = this;
> +
> +	me.method = me.isCreate ? 'POST' : 'PUT';
> +
> +	let ipanel = Ext.create('PVE.sdn.IpamEditInputPanel', {
> +	    isCreate: me.isCreate,
> +	});
> +
> +	Ext.apply(me, {
> +	    items: [
> +		ipanel,
> +	    ],
> +	});
> +
> +	me.callParent();
> +
> +	ipanel.setValues(me.mapping);
> +    },
> +});
> diff --git a/www/manager6/tree/DhcpTree.js b/www/manager6/tree/DhcpTree.js
> new file mode 100644
> index 000000000..ca279c29a
> --- /dev/null
> +++ b/www/manager6/tree/DhcpTree.js
> @@ -0,0 +1,267 @@
> +Ext.define('PVE.sdn.DhcpTree', {
> +    extend: 'Ext.tree.Panel',
> +    xtype: 'pveDhcpTree',
> +
> +    layout: 'fit',
> +    rootVisible: false,
> +    animate: false,
> +
> +    store: {
> +	sorters: ['ip', 'name'],
> +    },
> +
> +    controller: {
> +	xclass: 'Ext.app.ViewController',
> +
> +	reload: function() {
> +	    let me = this;
> +
> +	    Proxmox.Utils.API2Request({
> +		url: `/cluster/sdn/ipam`,
> +		method: 'GET',
> +		success: function(response, opts) {
> +		    let root = {
> +			name: '__root',
> +			expanded: true,
> +			children: [],
> +		    };
> +
> +		    let zones = {};
> +		    let vnets = {};
> +		    let subnets = {};
> +
> +		    response.result.data.forEach((element) => {
> +			element.leaf = true;
> +
> +			if (!(element.zone in zones)) {
> +			    let zone = {
> +				name: element.zone,
> +				type: 'zone',
> +				iconCls: 'fa fa-th',
> +				expanded: true,
> +				children: [],
> +			    };
> +
> +			    zones[element.zone] = zone;
> +			    root.children.push(zone);
> +			}
> +
> +			if (!(element.vnet in vnets)) {
> +			    let vnet = {
> +				name: element.vnet,
> +				zone: element.zone,
> +				type: 'vnet',
> +				iconCls: 'fa fa-network-wired x-fa-treepanel',
> +				expanded: true,
> +				children: [],
> +			    };
> +
> +			    vnets[element.vnet] = vnet;
> +			    zones[element.zone].children.push(vnet);
> +			}
> +
> +			if (!(element.subnet in subnets)) {
> +			    let subnet = {
> +				name: element.subnet,
> +				zone: element.zone,
> +				vnet: element.vnet,
> +				type: 'subnet',
> +				iconCls: 'x-tree-icon-none',
> +				expanded: true,
> +				children: [],
> +			    };
> +
> +			    subnets[element.subnet] = subnet;
> +			    vnets[element.vnet].children.push(subnet);
> +			}
> +
> +			element.type = 'mapping';
> +			element.iconCls = 'x-tree-icon-none';
> +			subnets[element.subnet].children.push(element);
> +		    });
> +
> +		    me.getView().setRootNode(root);
> +		},
> +	    });
> +	},
> +
> +	init: function(view) {
> +	    let me = this;
> +	    me.reload();
> +	},
> +
> +	onDelete: function(table, rI, cI, item, e, { data }) {
> +	    let me = this;
> +	    let view = me.getView();
> +
> +	    Ext.Msg.show({
> +		title: gettext('Confirm'),
> +		icon: Ext.Msg.WARNING,
> +		message: Ext.String.format(gettext('Are you sure you want to remove DHCP mapping {0}'), `${data.mac} / ${data.ip}`),
> +		buttons: Ext.Msg.YESNO,
> +		defaultFocus: 'no',
> +		callback: function(btn) {
> +		    if (btn !== 'yes') {
> +		        return;
> +		    }
> +
> +		    Proxmox.Utils.API2Request({
> +			url: `/cluster/sdn/ipam/${data.zone}/${data.vnet}/${data.mac}`,
> +			method: 'DELETE',
> +			waitMsgTarget: view,
> +			failure: function(response, opts) {
> +			    Ext.Msg.alert(gettext('Error'), response.htmlStatus);
> +			},
> +			callback: me.reload.bind(me),
> +		    });
> +		},
> +	    });
> +	},
> +
> +	editAction: function(_grid, _rI, _cI, _item, _e, rec) {
> +	    this.edit(rec);
> +	},
> +
> +	editDblClick: function() {
> +	    let me = this;
> +
> +	    let view = me.getView();
> +	    let selection = view.getSelection();
> +
> +	    if (!selection || selection.length < 1) {
> +		return;
> +	    }
> +
> +	    me.edit(selection[0]);
> +	},
> +
> +	edit: function(rec) {
> +	    let me = this;
> +
> +	    if (rec.data.type === 'mapping' && !rec.data.gateway) {
> +		me.openEditWindow(rec.data);
> +	    }
> +	},
> +
> +	openEditWindow: function(data) {
> +	    let me = this;
> +
> +	    Ext.create('PVE.sdn.IpamEdit', {
> +		autoShow: true,
> +		mapping: data,
> +		url: `/cluster/sdn/ipam`,
> +		extraRequestParams: {
> +		    vmid: data.vmid,
> +		    mac: data.mac,
> +		    zone: data.zone,
> +		    vnet: data.vnet,
> +		},
> +		listeners: {
> +		    destroy: () => me.reload(),
> +		},
> +	    });
> +	},
> +    },
> +
> +    listeners: {
> +	itemdblclick: 'editDblClick',
> +    },
> +
> +    tbar: [
> +	{
> +	    xtype: 'proxmoxButton',
> +	    text: gettext('Reload'),
> +	    handler: 'reload',
> +	},
> +    ],
> +
> +    columns: [
> +	{
> +	    xtype: 'treecolumn',
> +	    text: gettext('Name / VMID'),
> +	    dataIndex: 'name',
> +	    width: 200,
> +	    renderer: function(value, meta, record) {
> +		if (record.get('gateway')) {
> +		    return gettext('Gateway');
> +		}
> +
> +		return record.get('name') ?? record.get('vmid') ?? ' ';
> +	    },
> +	},
> +	{
> +	    text: gettext('IP'),
> +	    dataIndex: 'ip',
> +	    width: 200,
> +	},
> +	{
> +	    text: gettext('MAC'),
> +	    dataIndex: 'mac',
> +	    width: 200,
> +	},
> +	{
> +	    text: gettext('Gateway'),
> +	    dataIndex: 'gateway',
> +	    width: 200,
> +	},
> +	{
> +	    header: gettext('Actions'),
> +	    xtype: 'actioncolumn',
> +	    dataIndex: 'text',
> +	    width: 150,
> +	    items: [
> +		{
> +		    handler: function(table, rI, cI, item, e, { data }) {
> +			let me = this;
> +
> +			Ext.create('PVE.sdn.IpamEdit', {
> +			    autoShow: true,
> +			    mapping: {},
> +			    url: `/cluster/sdn/ipam`,
> +			    isCreate: true,
> +			    extraRequestParams: {
> +				vnet: data.name,
> +				zone: data.zone,
> +			    },
> +			    listeners: {
> +				destroy: () => {
> +				    me.up('pveDhcpTree').controller.reload();
> +				},
> +			    },
> +			});
> +		    },

i'd put that function in the controller like the other handlers here

> +		    getTip: (v, m, rec) => gettext('Add'),
> +		    getClass: (v, m, { data }) => {
> +			if (data.type === 'vnet') {
> +			    return 'fa fa-plus-square';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +                },
> +		{
> +		    handler: 'editAction',
> +		    getTip: (v, m, rec) => gettext('Edit'),
> +		    getClass: (v, m, { data }) => {
> +			if (data.type === 'mapping' && !data.gateway) {
> +			    return 'fa fa-pencil fa-fw';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +                },
> +		{
> +		    handler: 'onDelete',
> +		    getTip: (v, m, rec) => gettext('Delete'),
> +		    getClass: (v, m, { data }) => {
> +			if (data.type === 'mapping' && !data.gateway) {
> +			    return 'fa critical fa-trash-o';
> +			}
> +
> +			return 'pmx-hidden';
> +		    },
> +                },
> +	    ],
> +	},
> +    ],
> +});






More information about the pve-devel mailing list