* [pve-devel] [PATCH v2 container] api: network: get interfaces from containers
2023-06-15 9:43 [pve-devel] [PATCH v2 container manager] Show dynamic container IPs in GUI Leo Nunner
@ 2023-06-15 9:43 ` Leo Nunner
2023-11-14 16:43 ` DERUMIER, Alexandre
2023-11-14 18:23 ` [pve-devel] applied: " Thomas Lamprecht
2023-06-15 9:43 ` [pve-devel] [PATCH v2 manager 1/2] lxc: show dynamically assigned IPs in network tab Leo Nunner
2023-06-15 9:43 ` [pve-devel] [PATCH v2 manager 2/2] lxc: show IPs in summary view Leo Nunner
2 siblings, 2 replies; 9+ messages in thread
From: Leo Nunner @ 2023-06-15 9:43 UTC (permalink / raw)
To: pve-devel
Adds an 'interfaces' endpoint in the API
(/nodes/{node}/lxc/{vmid}/interfaces'), which returns a list of
interface names, together with a MAC, IPv4 and IPv6 address. This list
may be expanded in the future. Note that this is only returned for
*running* containers, stopped containers simply return an empty list.
Signed-off-by: Leo Nunner <l.nunner@proxmox.com>
---
src/PVE/API2/LXC.pm | 50 +++++++++++++++++++++++++++++++++++++++++++++
src/PVE/LXC.pm | 26 +++++++++++++++++++++++
2 files changed, 76 insertions(+)
diff --git a/src/PVE/API2/LXC.pm b/src/PVE/API2/LXC.pm
index 28d14de..8839105 100644
--- a/src/PVE/API2/LXC.pm
+++ b/src/PVE/API2/LXC.pm
@@ -2510,6 +2510,56 @@ __PACKAGE__->register_method({
return PVE::GuestHelpers::config_with_pending_array($conf, $pending_delete_hash);
}});
+__PACKAGE__->register_method({
+ name => 'ip',
+ path => '{vmid}/interfaces',
+ method => 'GET',
+ protected => 1,
+ permissions => {
+ check => ['perm', '/vms/{vmid}', [ 'VM.Audit' ]],
+ },
+ description => 'Get IP addresses of the specified container interface.',
+ parameters => {
+ additionalProperties => 0,
+ properties => {
+ node => get_standard_option('pve-node'),
+ vmid => get_standard_option('pve-vmid', { completion => \&PVE::LXC::complete_ctid }),
+ },
+ },
+ returns => {
+ type => "array",
+ items => {
+ type => 'object',
+ properties => {
+ name => {
+ type => 'string',
+ description => 'The name of the interface',
+ optional => 0,
+ },
+ hwaddr => {
+ type => 'string',
+ description => 'The MAC address of the interface',
+ optional => 0,
+ },
+ inet => {
+ type => 'string',
+ description => 'The IPv4 address of the interface',
+ optional => 1,
+ },
+ inet6 => {
+ type => 'string',
+ description => 'The IPv6 address of the interface',
+ optional => 1,
+ },
+ }
+ },
+ },
+ code => sub {
+ my ($param) = @_;
+
+ return PVE::LXC::get_interfaces($param->{vmid});
+ }});
+
__PACKAGE__->register_method({
name => 'mtunnel',
path => '{vmid}/mtunnel',
diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm
index a531ea5..611f5c2 100644
--- a/src/PVE/LXC.pm
+++ b/src/PVE/LXC.pm
@@ -1036,6 +1036,32 @@ sub hotplug_net {
PVE::LXC::Config->write_config($vmid, $conf);
}
+sub get_interfaces {
+ my ($vmid) = @_;
+
+ my $pid = eval { find_lxc_pid($vmid); };
+ return if $@;
+
+ my $output;
+ # enters the network namespace of the container and executes 'ip a'
+ run_command(['nsenter', '-t', $pid, '--net', '--', 'ip', '--json', 'a'],
+ outfunc => sub { $output .= shift; });
+
+ my $config = JSON::decode_json($output);
+
+ my $res;
+ for my $interface ($config->@*) {
+ my $obj = { name => $interface->{ifname} };
+ for my $ip ($interface->{addr_info}->@*) {
+ $obj->{$ip->{family}} = $ip->{local} . "/" . $ip->{prefixlen};
+ }
+ $obj->{hwaddr} = $interface->{address};
+ push @$res, $obj
+ }
+
+ return $res;
+}
+
sub update_ipconfig {
my ($vmid, $conf, $opt, $eth, $newnet, $rootdir) = @_;
--
2.30.2
^ permalink raw reply [flat|nested] 9+ messages in thread
* [pve-devel] [PATCH v2 manager 1/2] lxc: show dynamically assigned IPs in network tab
2023-06-15 9:43 [pve-devel] [PATCH v2 container manager] Show dynamic container IPs in GUI Leo Nunner
2023-06-15 9:43 ` [pve-devel] [PATCH v2 container] api: network: get interfaces from containers Leo Nunner
@ 2023-06-15 9:43 ` Leo Nunner
2023-11-15 20:51 ` DERUMIER, Alexandre
2023-06-15 9:43 ` [pve-devel] [PATCH v2 manager 2/2] lxc: show IPs in summary view Leo Nunner
2 siblings, 1 reply; 9+ messages in thread
From: Leo Nunner @ 2023-06-15 9:43 UTC (permalink / raw)
To: pve-devel
adds a call to /nodes/{node}/lxc/{vmid}/interfaces and merges the
returned data with the existing configuration. This will update the
IPv4 and IPv6 address, as well as the interface name (in case the
container changed it).
Signed-off-by: Leo Nunner <l.nunner@proxmox.com>
---
www/manager6/lxc/Network.js | 57 +++++++++++++++++++++++++++----------
1 file changed, 42 insertions(+), 15 deletions(-)
diff --git a/www/manager6/lxc/Network.js b/www/manager6/lxc/Network.js
index b2cd94109..41de72f43 100644
--- a/www/manager6/lxc/Network.js
+++ b/www/manager6/lxc/Network.js
@@ -356,25 +356,52 @@ Ext.define('PVE.lxc.NetworkView', {
Proxmox.Utils.setErrorMask(me, true);
+ let nodename = me.pveSelNode.data.node;
+ let vmid = me.pveSelNode.data.vmid;
+
Proxmox.Utils.API2Request({
- url: me.url,
+ url: `/nodes/${nodename}/lxc/${vmid}/interfaces`,
+ method: 'GET',
failure: function(response, opts) {
Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus);
},
- success: function(response, opts) {
- Proxmox.Utils.setErrorMask(me, false);
- let result = Ext.decode(response.responseText);
- me.dataCache = result.data || {};
- let records = [];
- for (const [key, value] of Object.entries(me.dataCache)) {
- if (key.match(/^net\d+/)) {
- let net = PVE.Parser.parseLxcNetwork(value);
- net.id = key;
- records.push(net);
- }
- }
- me.store.loadData(records);
- me.down('button[name=addButton]').setDisabled(records.length >= 32);
+ success: function(ifResponse, ifOpts) {
+ Proxmox.Utils.API2Request({
+ url: me.url,
+ failure: function(response, opts) {
+ Proxmox.Utils.setErrorMask(me, gettext('Error') + ': ' + response.htmlStatus);
+ },
+ success: function(confResponse, confOpts) {
+ Proxmox.Utils.setErrorMask(me, false);
+
+ let interfaces = [];
+ for (const [, iface] of Object.entries(ifResponse?.result?.data || {})) {
+ interfaces[iface.hwaddr] = iface;
+ }
+
+ let result = Ext.decode(confResponse.responseText);
+ me.dataCache = result.data || {};
+ let records = [];
+ for (const [key, value] of Object.entries(me.dataCache)) {
+ if (key.match(/^net\d+/)) {
+ let net = PVE.Parser.parseLxcNetwork(value);
+ net.id = key;
+
+ let iface;
+ if ((iface = interfaces[net.hwaddr.toLowerCase()])) {
+ net.name = iface.name;
+ net.ip = iface.inet;
+ net.ip6 = iface.inet6;
+ }
+
+ records.push(net);
+ }
+ }
+
+ me.store.loadData(records);
+ me.down('button[name=addButton]').setDisabled(records.length >= 32);
+ },
+ });
},
});
},
--
2.30.2
^ permalink raw reply [flat|nested] 9+ messages in thread
* [pve-devel] [PATCH v2 manager 2/2] lxc: show IPs in summary view
2023-06-15 9:43 [pve-devel] [PATCH v2 container manager] Show dynamic container IPs in GUI Leo Nunner
2023-06-15 9:43 ` [pve-devel] [PATCH v2 container] api: network: get interfaces from containers Leo Nunner
2023-06-15 9:43 ` [pve-devel] [PATCH v2 manager 1/2] lxc: show dynamically assigned IPs in network tab Leo Nunner
@ 2023-06-15 9:43 ` Leo Nunner
2 siblings, 0 replies; 9+ messages in thread
From: Leo Nunner @ 2023-06-15 9:43 UTC (permalink / raw)
To: pve-devel
modelled after the QEMU Guest Agent UI. We only show the first
non-loopback IP on the summary page itself.
Signed-off-by: Leo Nunner <l.nunner@proxmox.com>
---
www/manager6/Makefile | 1 +
www/manager6/lxc/ContainerIPView.js | 194 ++++++++++++++++++++++++++
www/manager6/panel/GuestStatusView.js | 12 +-
www/manager6/panel/GuestSummary.js | 2 +-
4 files changed, 207 insertions(+), 2 deletions(-)
create mode 100644 www/manager6/lxc/ContainerIPView.js
diff --git a/www/manager6/Makefile b/www/manager6/Makefile
index 71ab928ff..2e967d949 100644
--- a/www/manager6/Makefile
+++ b/www/manager6/Makefile
@@ -180,6 +180,7 @@ JSSRC= \
lxc/ResourceEdit.js \
lxc/Resources.js \
lxc/MultiMPEdit.js \
+ lxc/ContainerIPView.js \
menu/MenuItem.js \
menu/TemplateMenu.js \
ceph/CephInstallWizard.js \
diff --git a/www/manager6/lxc/ContainerIPView.js b/www/manager6/lxc/ContainerIPView.js
new file mode 100644
index 000000000..69b107af3
--- /dev/null
+++ b/www/manager6/lxc/ContainerIPView.js
@@ -0,0 +1,194 @@
+Ext.define('PVE.window.ContainerIPInfo', {
+ extend: 'Ext.window.Window',
+ width: 600,
+ title: gettext('Container Network Information'),
+ height: 300,
+ layout: {
+ type: 'fit',
+ },
+ modal: true,
+ items: [
+ {
+ xtype: 'grid',
+ store: {},
+ emptyText: gettext('No network information'),
+ columns: [
+ {
+ dataIndex: 'name',
+ text: gettext('Name'),
+ flex: 2,
+ },
+ {
+ dataIndex: 'hwaddr',
+ text: gettext('MAC address'),
+ width: 140,
+ },
+ {
+ dataIndex: 'inet',
+ text: gettext('IPv4 address'),
+ align: 'right',
+ flex: 3,
+ },
+ {
+ dataIndex: 'inet6',
+ text: gettext('IPv6 address'),
+ align: 'right',
+ flex: 4,
+ },
+ ],
+ },
+ ],
+});
+
+Ext.define('PVE.lxc.IPView', {
+ extend: 'Ext.container.Container',
+ xtype: 'pveContainerIPView',
+
+ layout: {
+ type: 'hbox',
+ align: 'top',
+ },
+
+ items: [
+ {
+ xtype: 'box',
+ html: '<i class="fa fa-exchange"></i> IPs',
+ },
+ {
+ xtype: 'container',
+ flex: 1,
+ layout: {
+ type: 'vbox',
+ align: 'right',
+ pack: 'end',
+ },
+ items: [
+ {
+ xtype: 'label',
+ flex: 1,
+ itemId: 'ipBox',
+ style: {
+ 'text-align': 'right',
+ },
+ },
+ {
+ xtype: 'button',
+ itemId: 'moreBtn',
+ hidden: true,
+ ui: 'default-toolbar',
+ handler: function(btn) {
+ let view = this.up('pveContainerIPView');
+
+ var win = Ext.create('PVE.window.ContainerIPInfo');
+ win.down('grid').getStore().setData(view.ifaces);
+ win.show();
+ },
+ text: gettext('More'),
+ },
+ ],
+ },
+ ],
+
+ getDefaultIps: function(ifaces) {
+ var me = this;
+ var ips = [];
+ ifaces.forEach(function(iface) {
+ // We only want to show the first non-loopback interface
+ if (!ips.length &&
+ iface.data.hwaddr &&
+ iface.data.hwaddr !== '00:00:00:00:00:00' &&
+ iface.data.hwaddr !== '0:0:0:0:0:0') {
+ ips.push(iface.data.inet);
+ ips.push(iface.data.inet6);
+ }
+ });
+
+ return ips;
+ },
+
+ startIPStore: function(store, records, success) {
+ var me = this;
+ let state = store.getById('status');
+
+ me.running = state && state.data.value === 'running';
+
+ var caps = Ext.state.Manager.get('GuiCap');
+
+ if (!caps.vms['VM.Monitor']) {
+ var errorText = gettext("Requires '{0}' Privileges");
+ me.updateStatus(false, Ext.String.format(errorText, 'VM.Monitor'));
+ return;
+ }
+
+ if (me.running && me.ipStore.isStopped) {
+ me.ipStore.startUpdate();
+ } else if (me.ipStore.isStopped) {
+ me.updateStatus();
+ }
+ },
+
+ updateStatus: function(unsuccessful, defaulttext) {
+ var me = this;
+ var text = defaulttext || gettext('No network information');
+ var more = false;
+ if (Ext.isArray(me.ifaces) && me.ifaces.length) {
+ more = true;
+ var ips = me.getDefaultIps(me.ifaces);
+ if (ips.length !== 0) {
+ text = ips.join('<br>');
+ }
+ }
+
+ var ipBox = me.down('#ipBox');
+ ipBox.update(text);
+
+ var moreBtn = me.down('#moreBtn');
+ moreBtn.setVisible(more);
+ },
+
+ initComponent: function() {
+ var me = this;
+
+ if (!me.rstore) {
+ throw 'rstore not given';
+ }
+
+ if (!me.pveSelNode) {
+ throw 'pveSelNode not given';
+ }
+
+ var nodename = me.pveSelNode.data.node;
+ var vmid = me.pveSelNode.data.vmid;
+
+ me.ipStore = Ext.create('Proxmox.data.UpdateStore', {
+ interval: 10000,
+ storeid: 'lxc-interfaces-' + vmid,
+ method: 'GET',
+ proxy: {
+ type: 'proxmox',
+ url: '/api2/json/nodes/' + nodename + '/lxc/' + vmid + '/interfaces',
+ },
+ });
+
+ me.callParent();
+
+ me.mon(me.ipStore, 'load', function(store, records, success) {
+ if (records && records.length) {
+ me.ifaces = records;
+ } else {
+ me.ifaces = undefined;
+ }
+ me.updateStatus(!success);
+ });
+
+ me.on('destroy', me.ipStore.stopUpdate, me.ipStore);
+
+ // if we already have info about the guest, use it immediately
+ if (me.rstore.getCount()) {
+ me.startIPStore(me.rstore, me.rstore.getData(), false);
+ }
+
+ // check if the guest agent is there on every statusstore load
+ me.mon(me.rstore, 'load', me.startIPStore, me);
+ },
+});
diff --git a/www/manager6/panel/GuestStatusView.js b/www/manager6/panel/GuestStatusView.js
index 8db1f492c..7a093bb34 100644
--- a/www/manager6/panel/GuestStatusView.js
+++ b/www/manager6/panel/GuestStatusView.js
@@ -113,7 +113,7 @@ Ext.define('PVE.panel.GuestStatusView', {
height: 15,
},
{
- itemId: 'ips',
+ itemId: 'agentIPs',
xtype: 'pveAgentIPView',
cbind: {
rstore: '{rstore}',
@@ -122,6 +122,16 @@ Ext.define('PVE.panel.GuestStatusView', {
disabled: '{isLxc}',
},
},
+ {
+ itemId: 'ctIPS',
+ xtype: 'pveContainerIPView',
+ cbind: {
+ rstore: '{rstore}',
+ pveSelNode: '{pveSelNode}',
+ hidden: '{!isLxc}',
+ disabled: '{!isLxc}',
+ },
+ },
],
updateTitle: function() {
diff --git a/www/manager6/panel/GuestSummary.js b/www/manager6/panel/GuestSummary.js
index 1565db3f6..049b63aa9 100644
--- a/www/manager6/panel/GuestSummary.js
+++ b/www/manager6/panel/GuestSummary.js
@@ -54,7 +54,7 @@ Ext.define('PVE.guest.Summary', {
items = [
{
xtype: 'container',
- height: 300,
+ height: 350,
layout: {
type: 'hbox',
align: 'stretch',
--
2.30.2
^ permalink raw reply [flat|nested] 9+ messages in thread