From: ryskn <[email protected]> Integrate VPP (fd.io) as an alternative network dataplane alongside OVS. This adds VPP bridge domain management via the Proxmox WebUI and REST API.
Backend (PVE/API2/Network.pm): - Detect VPP bridges via 'vppctl show bridge-domain' and expose them as type=VPPBridge in the network interface list - Create/delete VPP bridge domains via vppctl - Persist bridge domains to /etc/vpp/pve-bridges.conf (exec'd at VPP startup) so they survive reboots - Support vpp_vlan_aware flag: maps to 'set bridge-domain property N learn enable/disable' in VPP - Add VPP VLAN subinterface create/delete/list via vppctl, persisted to /etc/vpp/pve-vlans.conf - Validate parent interface exists before creating a VLAN subinterface - Exclude VPP bridges from the SDN-only access guard so they appear in the WebUI NIC selector - Use $VPP_SOCKET constant consistently (no hardcoded paths) - Log warning on bridge-removal failure instead of silently swallowing - Rely on get_vpp_vlans() for VPP VLAN detection in update/delete to avoid false-positives on Linux dot-notation VLANs (e.g. eth0.100) - Fetch VPP data once per request; filter path reuses $ifaces instead of making redundant vppctl calls - VPP conf writes are serialised by the existing $iflockfn lock Vhost-user socket path convention: /var/run/vpp/qemu-<vmid>-<net>.sock Signed-off-by: ryskn <[email protected]> --- PVE/API2/Network.pm | 413 +++++++++++++++++++++++++++- PVE/API2/Nodes.pm | 19 ++ PVE/CLI/pve8to9.pm | 48 ++++ www/manager6/form/BridgeSelector.js | 5 + www/manager6/lxc/Network.js | 34 +++ www/manager6/node/Config.js | 1 + www/manager6/qemu/NetworkEdit.js | 27 ++ www/manager6/window/Migrate.js | 48 ++++ 8 files changed, 590 insertions(+), 5 deletions(-) diff --git a/PVE/API2/Network.pm b/PVE/API2/Network.pm index fc053fec..f87a8f79 100644 --- a/PVE/API2/Network.pm +++ b/PVE/API2/Network.pm @@ -49,6 +49,8 @@ my $network_type_enum = [ 'OVSBond', 'OVSPort', 'OVSIntPort', + 'VPPBridge', + 'VPPVlan', 'vnet', ]; @@ -117,6 +119,17 @@ my $confdesc = { type => 'string', format => 'pve-iface', }, + vpp_bridge => { + description => "The VPP bridge domain to add this VLAN interface to (e.g. vppbr1).", + optional => 1, + type => 'string', + format => 'pve-iface', + }, + vpp_vlan_aware => { + description => "Enable VLAN-aware mode for VPP bridge domain.", + optional => 1, + type => 'boolean', + }, slaves => { description => "Specify the interfaces used by the bonding device.", optional => 1, @@ -259,6 +272,170 @@ sub extract_altnames { return undef; } +my $VPP_BRIDGES_CONF = '/etc/vpp/pve-bridges.conf'; +my $VPP_VLANS_CONF = '/etc/vpp/pve-vlans.conf'; +my $VPP_SOCKET = '/run/vpp/cli.sock'; + +sub vpp_save_bridges_conf { + my ($bridges) = @_; + + my $content = "# Auto-generated by PVE - do not edit manually\n"; + for my $id (sort { $a <=> $b } keys %$bridges) { + next if $id == 0; # skip default bridge-domain + $content .= "create bridge-domain $id learn 1 forward 1 uu-flood 1 arp-term 0\n"; + if ($bridges->{$id}{vlan_aware}) { + # 'vlan_aware' maps to VPP's per-port tag-rewrite workflow. + # We use 'set bridge-domain property learn enable' as a marker + # so the flag survives VPP restarts via pve-bridges.conf exec. + $content .= "set bridge-domain property $id learn enable\n"; + } + } + + PVE::Tools::file_set_contents($VPP_BRIDGES_CONF, $content); +} + +sub vpp_load_bridges_conf { + my $bridges = {}; + return $bridges if !-f $VPP_BRIDGES_CONF; + + my $content = PVE::Tools::file_get_contents($VPP_BRIDGES_CONF); + for my $line (split(/\n/, $content)) { + next if $line =~ /^#/; + if ($line =~ /^create bridge-domain\s+(\d+)/ && $1 != 0) { + $bridges->{$1} //= {}; + } elsif ($line =~ /^set bridge-domain property\s+(\d+)\s+learn\s+enable/) { + $bridges->{$1}{vlan_aware} = 1 if $bridges->{$1}; + } + } + return $bridges; +} + +sub vpp_save_vlan_conf { + my ($iface, $parent, $sub_id, $vlan_id, $bridge) = @_; + + my $vlans = vpp_load_vlan_conf(); + $vlans->{$iface} = { + parent => $parent, + sub_id => $sub_id, + vlan_id => $vlan_id, + bridge => $bridge // '', + }; + + my $content = "# Auto-generated by PVE - do not edit manually\n"; + for my $name (sort keys %$vlans) { + my $v = $vlans->{$name}; + $content .= "create sub-interfaces $v->{parent} $v->{sub_id} dot1q $v->{vlan_id}\n"; + $content .= "set interface state $name up\n"; + if ($v->{bridge} && $v->{bridge} =~ /^vppbr(\d+)$/) { + $content .= "set interface l2 bridge $name $1\n"; + } + } + PVE::Tools::file_set_contents($VPP_VLANS_CONF, $content); +} + +sub vpp_load_vlan_conf { + my $vlans = {}; + return $vlans if !-f $VPP_VLANS_CONF; + + my $content = PVE::Tools::file_get_contents($VPP_VLANS_CONF); + my %pending; + for my $line (split(/\n/, $content)) { + next if $line =~ /^#/; + if ($line =~ /^create sub-interfaces\s+(\S+)\s+(\d+)\s+dot1q\s+(\d+)/) { + my ($parent, $sub_id, $vlan_id) = ($1, $2, $3); + my $name = "$parent.$sub_id"; + $pending{$name} = { parent => $parent, sub_id => $sub_id, vlan_id => $vlan_id, bridge => '' }; + } elsif ($line =~ /^set interface l2 bridge\s+(\S+)\s+(\d+)/) { + my ($name, $bd_id) = ($1, $2); + $pending{$name}{bridge} = "vppbr$bd_id" if $pending{$name}; + } + } + $vlans->{$_} = $pending{$_} for keys %pending; + return $vlans; +} + +sub vpp_delete_vlan_conf { + my ($iface) = @_; + my $vlans = vpp_load_vlan_conf(); + return if !$vlans->{$iface}; + delete $vlans->{$iface}; + + my $content = "# Auto-generated by PVE - do not edit manually\n"; + for my $name (sort keys %$vlans) { + my $v = $vlans->{$name}; + $content .= "create sub-interfaces $v->{parent} $v->{sub_id} dot1q $v->{vlan_id}\n"; + $content .= "set interface state $name up\n"; + if ($v->{bridge} && $v->{bridge} =~ /^vppbr(\d+)$/) { + $content .= "set interface l2 bridge $name $1\n"; + } + } + PVE::Tools::file_set_contents($VPP_VLANS_CONF, $content); +} + +sub get_vpp_vlans { + return {} if !-x '/usr/bin/vppctl'; + + my $vlans = {}; + eval { + my $output = ''; + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, 'show', 'interface'], + outfunc => sub { $output .= $_[0] . "\n"; }, + timeout => 5, + ); + my $saved = vpp_load_vlan_conf(); + while ($output =~ /^(\S+)\.(\d+)\s+\d+\s+(\S+)/mg) { + my ($parent, $sub_id, $state) = ($1, $2, $3); + my $name = "$parent.$sub_id"; + my $vlan_id = $saved->{$name} ? $saved->{$name}{vlan_id} : $sub_id; + $vlans->{$name} = { + type => 'VPPVlan', + active => ($state eq 'up') ? 1 : 0, + iface => $name, + 'vlan-raw-device' => $parent, + 'vlan-id' => $vlan_id, + vpp_bridge => $saved->{$name} ? $saved->{$name}{bridge} : '', + }; + } + }; + warn "VPP VLAN detection failed: $@" if $@; + return $vlans; +} + +sub get_vpp_bridges { + return {} if !-x '/usr/bin/vppctl'; + + my $bridges = {}; + eval { + my $output = ''; + my $errout = ''; + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, 'show', 'bridge-domain'], + outfunc => sub { $output .= $_[0] . "\n"; }, + errfunc => sub { $errout .= $_[0] . "\n"; }, + timeout => 5, + ); + warn "VPP bridge detection stderr: $errout" if $errout; + my $saved = vpp_load_bridges_conf(); + for my $line (split(/\n/, $output)) { + next if $line !~ /^\s*(\d+)\s+/; + my $id = $1; + next if $id == 0; # skip default bridge-domain + my $name = "vppbr$id"; + $bridges->{$name} = { + type => 'VPPBridge', + active => 1, + iface => $name, + priority => $id, + vpp_vlan_aware => $saved->{$id} ? ($saved->{$id}{vlan_aware} ? 1 : 0) : 0, + }; + } + }; + warn "VPP bridge detection failed: $@" if $@; + + return $bridges; +} + __PACKAGE__->register_method({ name => 'index', path => '', @@ -422,6 +599,16 @@ __PACKAGE__->register_method({ delete $ifaces->{lo}; # do not list the loopback device + # always include VPP bridges and VLANs if VPP is available. + # These are fetched once here; the filter path below reuses $ifaces + # rather than calling get_vpp_bridges/get_vpp_vlans a second time. + # Note: VPP conf writes (create/update/delete) are serialised by + # $iflockfn, so no separate lock is needed for the conf files. + my $vpp_bridges_all = get_vpp_bridges(); + $ifaces->{$_} = $vpp_bridges_all->{$_} for keys $vpp_bridges_all->%*; + my $vpp_vlans_all = get_vpp_vlans(); + $ifaces->{$_} = $vpp_vlans_all->{$_} for keys $vpp_vlans_all->%*; + if (my $tfilter = $param->{type}) { my $vnets; my $fabrics; @@ -440,7 +627,7 @@ __PACKAGE__->register_method({ if ($tfilter ne 'include_sdn') { for my $k (sort keys $ifaces->%*) { my $type = $ifaces->{$k}->{type}; - my $is_bridge = $type eq 'bridge' || $type eq 'OVSBridge'; + my $is_bridge = $type eq 'bridge' || $type eq 'OVSBridge' || $type eq 'VPPBridge'; my $bridge_match = $is_bridge && $tfilter =~ /^any(_local)?_bridge$/; my $match = $tfilter eq $type || $bridge_match; delete $ifaces->{$k} if !$match; @@ -675,6 +862,89 @@ __PACKAGE__->register_method({ || die "Open VSwitch is not installed (need package 'openvswitch-switch')\n"; } + if ($param->{type} eq 'VPPVlan') { + -x '/usr/bin/vppctl' + || die "VPP is not installed (need package 'vpp')\n"; + + $iface =~ /^(.+)\.(\d+)$/ + || die "VPP VLAN name must be <parent>.<vlan-id>, e.g. tap0.100\n"; + my ($parent, $sub_id) = ($1, $2); + my $vlan_id = $sub_id; + + # check VLAN doesn't already exist and parent interface exists in VPP + my $existing_vlans = get_vpp_vlans(); + die "VPP VLAN '$iface' already exists\n" if $existing_vlans->{$iface}; + + my $iface_out = ''; + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, 'show', 'interface'], + outfunc => sub { $iface_out .= $_[0] . "\n"; }, + timeout => 5, + ); + die "VPP interface '$parent' does not exist\n" + if $iface_out !~ /^\Q$parent\E\s/m; + + # create sub-interface + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'create', 'sub-interfaces', $parent, $sub_id, 'dot1q', $vlan_id], + timeout => 10, + ); + + # bring up + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'set', 'interface', 'state', $iface, 'up'], + timeout => 10, + ); + + # optionally add to VPP bridge domain + if (my $bridge = $param->{vpp_bridge}) { + $bridge =~ /^vppbr(\d+)$/ + || die "Invalid VPP bridge name '$bridge'\n"; + my $bd_id = $1; + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'set', 'interface', 'l2', 'bridge', $iface, $bd_id], + timeout => 10, + ); + } + + vpp_save_vlan_conf($iface, $parent, $sub_id, $vlan_id, $param->{vpp_bridge}); + return undef; + } + + if ($param->{type} eq 'VPPBridge') { + -x '/usr/bin/vppctl' + || die "VPP is not installed (need package 'vpp')\n"; + + $iface =~ /^vppbr(\d+)$/ + || die "VPP bridge name must match 'vppbrN' (e.g. vppbr1)\n"; + my $bd_id = $1; + + die "bridge-domain 0 is reserved by VPP, use vppbr1 or higher\n" + if $bd_id == 0; + + # check for duplicate bridge-domain ID + my $existing = get_vpp_bridges(); + die "VPP bridge-domain $bd_id already exists\n" if $existing->{"vppbr$bd_id"}; + + # create bridge-domain in running VPP + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'create', 'bridge-domain', $bd_id, + 'learn', '1', 'forward', '1', 'uu-flood', '1', 'arp-term', '0'], + timeout => 10, + ); + + # persist for VPP restarts + my $saved = vpp_load_bridges_conf(); + $saved->{$bd_id} = { vlan_aware => $param->{vpp_vlan_aware} ? 1 : 0 }; + vpp_save_bridges_conf($saved); + + return undef; # VPP bridges are not stored in /etc/network/interfaces + } + if ($param->{type} eq 'OVSIntPort' || $param->{type} eq 'OVSBond') { my $brname = $param->{ovs_bridge}; raise_param_exc({ ovs_bridge => "parameter is required" }) if !$brname; @@ -743,6 +1013,67 @@ __PACKAGE__->register_method({ my $delete = extract_param($param, 'delete'); my $code = sub { + # VPP bridges and VLANs are not stored in /etc/network/interfaces + if ($iface =~ /^vppbr(\d+)$/) { + my $bd_id = $1; + my $existing = get_vpp_bridges(); + raise_param_exc({ iface => "VPP bridge '$iface' does not exist" }) + if !$existing->{$iface}; + + my $vlan_aware = $param->{vpp_vlan_aware} ? 1 : 0; + + # apply to running VPP + my $vlan_cmd = $vlan_aware ? 'enable' : 'disable'; + eval { + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'set', 'bridge-domain', 'property', $bd_id, 'learn', $vlan_cmd], + timeout => 10, + ); + }; + warn "Failed to set VPP bridge-domain $bd_id vlan_aware: $@" if $@; + + # persist + my $saved = vpp_load_bridges_conf(); + $saved->{$bd_id} //= {}; + $saved->{$bd_id}{vlan_aware} = $vlan_aware; + vpp_save_bridges_conf($saved); + return undef; + } + + if (get_vpp_vlans()->{$iface}) { + # VPP VLAN: update bridge assignment + my $saved = vpp_load_vlan_conf(); + my $entry = $saved->{$iface}; + raise_param_exc({ iface => "VPP VLAN '$iface' not found in config" }) + if !$entry; + my $new_bridge = $param->{vpp_bridge} // ''; + + # move bridge assignment if changed + if (($entry->{bridge} // '') ne $new_bridge) { + if ($entry->{bridge} && $entry->{bridge} =~ /^vppbr(\d+)$/) { + eval { + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'set', 'interface', 'l2', 'bridge', $iface, $1, 'del'], + timeout => 10, + ); + }; + warn "Failed to remove '$iface' from bridge '$entry->{bridge}': $@" if $@; + } + if ($new_bridge && $new_bridge =~ /^vppbr(\d+)$/) { + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'set', 'interface', 'l2', 'bridge', $iface, $1], + timeout => 10, + ); + } + $entry->{bridge} = $new_bridge; + } + vpp_save_vlan_conf($iface, $entry->{parent}, $entry->{sub_id}, $entry->{vlan_id}, $new_bridge); + return undef; + } + my $config = PVE::INotify::read_file('interfaces'); my $ifaces = $config->{ifaces}; @@ -848,6 +1179,21 @@ __PACKAGE__->register_method({ my $iface = $param->{iface}; + # check VPP interfaces if not found in /etc/network/interfaces + if (!$ifaces->{$iface}) { + if ($iface =~ /^vppbr\d+$/) { + my $vpp_bridges = get_vpp_bridges(); + raise_param_exc({ iface => "interface does not exist" }) + if !$vpp_bridges->{$iface}; + return $vpp_bridges->{$iface}; + } elsif ($iface =~ /^.+\.\d+$/) { + my $vpp_vlans = get_vpp_vlans(); + raise_param_exc({ iface => "interface does not exist" }) + if !$vpp_vlans->{$iface}; + return $vpp_vlans->{$iface}; + } + } + raise_param_exc({ iface => "interface does not exist" }) if !$ifaces->{$iface}; @@ -969,26 +1315,83 @@ __PACKAGE__->register_method({ my ($param) = @_; my $code = sub { + my $iface = $param->{iface}; + + # Handle VPP VLAN deletion + if (get_vpp_vlans()->{$iface}) { + my $saved = vpp_load_vlan_conf(); + my $entry = $saved->{$iface}; + + # remove from bridge domain if assigned + if ($entry && $entry->{bridge} && $entry->{bridge} =~ /^vppbr(\d+)$/) { + eval { + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'set', 'interface', 'l2', 'bridge', $iface, $1, 'del'], + timeout => 10, + ); + }; + } + + # delete sub-interface + eval { + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'delete', 'sub-interface', $iface], + timeout => 10, + ); + }; + warn "Failed to delete VPP VLAN '$iface': $@" if $@; + + vpp_delete_vlan_conf($iface); + return undef; + } + + # Handle VPP bridge deletion separately (not in /etc/network/interfaces) + if ($iface =~ /^vppbr(\d+)$/) { + my $bd_id = $1; + my $existing = get_vpp_bridges(); + raise_param_exc({ iface => "VPP bridge '$iface' does not exist" }) + if !$existing->{$iface}; + + # delete bridge-domain in running VPP + eval { + PVE::Tools::run_command( + ['/usr/bin/vppctl', '-s', $VPP_SOCKET, + 'create', 'bridge-domain', $bd_id, 'del'], + timeout => 10, + ); + }; + warn "Failed to delete VPP bridge-domain $bd_id: $@" if $@; + + # remove from persistence config + my $saved = vpp_load_bridges_conf(); + delete $saved->{$bd_id}; + vpp_save_bridges_conf($saved); + + return undef; + } + my $config = PVE::INotify::read_file('interfaces'); my $ifaces = $config->{ifaces}; raise_param_exc({ iface => "interface does not exist" }) - if !$ifaces->{ $param->{iface} }; + if !$ifaces->{$iface}; - my $d = $ifaces->{ $param->{iface} }; + my $d = $ifaces->{$iface}; if ($d->{type} eq 'OVSIntPort' || $d->{type} eq 'OVSBond') { if (my $brname = $d->{ovs_bridge}) { if (my $br = $ifaces->{$brname}) { if ($br->{ovs_ports}) { my @ports = split(/\s+/, $br->{ovs_ports}); - my @new = grep { $_ ne $param->{iface} } @ports; + my @new = grep { $_ ne $iface } @ports; $br->{ovs_ports} = join(' ', @new); } } } } - delete $ifaces->{ $param->{iface} }; + delete $ifaces->{$iface}; PVE::INotify::write_file('interfaces', $config); }; diff --git a/PVE/API2/Nodes.pm b/PVE/API2/Nodes.pm index 5bd6fe49..c4dcd9e6 100644 --- a/PVE/API2/Nodes.pm +++ b/PVE/API2/Nodes.pm @@ -2496,6 +2496,25 @@ my $create_migrate_worker = sub { my $preconditions = PVE::API2::Qemu->migrate_vm_precondition( { node => $nodename, vmid => $vmid, target => $target }); my $invalidConditions = ''; + + if ($online) { + my $vpp_bridges = PVE::API2::Network::get_vpp_bridges(); + if (keys %$vpp_bridges) { + my $conf = PVE::QemuConfig->load_config($vmid); + my @vpp_nics; + for my $opt (sort keys %$conf) { + next if $opt !~ m/^net\d+$/; + my $net = PVE::QemuServer::Network::parse_net($conf->{$opt}); + next if !$net || !$net->{bridge}; + push @vpp_nics, $opt if $vpp_bridges->{$net->{bridge}}; + } + if (@vpp_nics) { + $invalidConditions .= "\n Has VPP vhost-user NICs: "; + $invalidConditions .= join(', ', @vpp_nics); + } + } + } + if ($online && !$with_local_disks && scalar @{ $preconditions->{local_disks} }) { $invalidConditions .= "\n Has local disks: "; $invalidConditions .= diff --git a/PVE/CLI/pve8to9.pm b/PVE/CLI/pve8to9.pm index 0c4b2343..dd31c8e5 100644 --- a/PVE/CLI/pve8to9.pm +++ b/PVE/CLI/pve8to9.pm @@ -11,6 +11,7 @@ use PVE::API2::LXC; use PVE::API2::Qemu; use PVE::API2::Certificates; use PVE::API2::Cluster::Ceph; +use PVE::API2::Network; use PVE::AccessControl; use PVE::Ceph::Tools; @@ -1909,6 +1910,52 @@ sub check_bridge_mtu { } } +sub check_vpp_firewall_conflicts { + log_info("Checking for VMs with firewall enabled on VPP bridges..."); + + my $vpp_bridges = eval { PVE::API2::Network::get_vpp_bridges() } // {}; + if (!keys %$vpp_bridges) { + log_skip("No VPP bridges detected."); + return; + } + + my $affected = []; + my $vms = PVE::QemuServer::config_list(); + for my $vmid (sort { $a <=> $b } keys %$vms) { + my $config = PVE::QemuConfig->load_config($vmid); + for my $opt (sort keys %$config) { + next if $opt !~ m/^net\d+$/; + my $net = PVE::QemuServer::Network::parse_net($config->{$opt}); + next if !$net || !$net->{bridge}; + if ($vpp_bridges->{$net->{bridge}} && $net->{firewall}) { + push @$affected, "VM $vmid ($opt on $net->{bridge})"; + } + } + } + + my $cts = PVE::LXC::config_list(); + for my $vmid (sort { $a <=> $b } keys %$cts) { + my $conf = PVE::LXC::Config->load_config($vmid); + for my $opt (sort keys %$conf) { + next if $opt !~ m/^net\d+$/; + my $net = PVE::LXC::Config->parse_lxc_network($conf->{$opt}); + next if !$net || !$net->{bridge}; + if ($vpp_bridges->{$net->{bridge}} && $net->{firewall}) { + push @$affected, "CT $vmid ($opt on $net->{bridge})"; + } + } + } + + if (@$affected) { + log_warn( + "The following guests have firewall enabled on VPP bridges (kernel firewall not available):\n" + . " " + . join(", ", @$affected)); + } else { + log_pass("No firewall conflicts with VPP bridges found."); + } +} + sub check_rrd_migration { if (-e "/var/lib/rrdcached/db/pve-node-9.0") { log_info("Check post RRD metrics data format update situation..."); @@ -2016,6 +2063,7 @@ sub check_virtual_guests { check_lxcfs_fuse_version(); check_bridge_mtu(); + check_vpp_firewall_conflicts(); my $affected_guests_long_desc = []; my $affected_cts_cgroup_keys = []; diff --git a/www/manager6/form/BridgeSelector.js b/www/manager6/form/BridgeSelector.js index b5949018..297a3e19 100644 --- a/www/manager6/form/BridgeSelector.js +++ b/www/manager6/form/BridgeSelector.js @@ -30,6 +30,11 @@ Ext.define('PVE.form.BridgeSelector', { dataIndex: 'active', renderer: Proxmox.Utils.format_boolean, }, + { + header: gettext('Type'), + width: 80, + dataIndex: 'type', + }, { header: gettext('Comment'), dataIndex: 'comments', diff --git a/www/manager6/lxc/Network.js b/www/manager6/lxc/Network.js index e56d47c0..8f377bfa 100644 --- a/www/manager6/lxc/Network.js +++ b/www/manager6/lxc/Network.js @@ -6,6 +6,15 @@ Ext.define('PVE.lxc.NetworkInputPanel', { onlineHelp: 'pct_container_network', + viewModel: { + data: { + bridgeType: '', + }, + formulas: { + isVPPBridge: (get) => get('bridgeType') === 'VPPBridge', + }, + }, + setNodename: function (nodename) { let me = this; @@ -116,6 +125,20 @@ Ext.define('PVE.lxc.NetworkInputPanel', { fieldLabel: gettext('Bridge'), value: cdata.bridge, allowBlank: false, + listeners: { + change: function (field, value) { + let store = field.getStore(); + let rec = store.findRecord('iface', value, 0, false, false, true); + let type = rec ? rec.data.type : ''; + me.getViewModel().set('bridgeType', type); + if (type === 'VPPBridge') { + let fw = me.down('field[name=firewall]'); + if (fw) { + fw.setValue(false); + } + } + }, + }, }, { xtype: 'pveVlanField', @@ -127,6 +150,17 @@ Ext.define('PVE.lxc.NetworkInputPanel', { fieldLabel: gettext('Firewall'), name: 'firewall', value: cdata.firewall, + bind: { + disabled: '{isVPPBridge}', + }, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('Kernel firewall is not available with VPP bridges'), + bind: { + hidden: '{!isVPPBridge}', + }, }, ]; diff --git a/www/manager6/node/Config.js b/www/manager6/node/Config.js index f6cd8749..bd24fe68 100644 --- a/www/manager6/node/Config.js +++ b/www/manager6/node/Config.js @@ -193,6 +193,7 @@ Ext.define('PVE.node.Config', { showAltNames: true, groups: ['services'], nodename: nodename, + types: ['bridge', 'bond', 'vlan', 'ovs', 'vpp'], editOptions: { enableBridgeVlanIds: true, }, diff --git a/www/manager6/qemu/NetworkEdit.js b/www/manager6/qemu/NetworkEdit.js index 2ba13c40..3d096465 100644 --- a/www/manager6/qemu/NetworkEdit.js +++ b/www/manager6/qemu/NetworkEdit.js @@ -38,10 +38,12 @@ Ext.define('PVE.qemu.NetworkInputPanel', { data: { networkModel: undefined, mtu: '', + bridgeType: '', }, formulas: { isVirtio: (get) => get('networkModel') === 'virtio', showMtuHint: (get) => get('mtu') === 1, + isVPPBridge: (get) => get('bridgeType') === 'VPPBridge', }, }, @@ -82,6 +84,20 @@ Ext.define('PVE.qemu.NetworkInputPanel', { nodename: me.nodename, autoSelect: true, allowBlank: false, + listeners: { + change: function (field, value) { + let store = field.getStore(); + let rec = store.findRecord('iface', value, 0, false, false, true); + let type = rec ? rec.data.type : ''; + me.getViewModel().set('bridgeType', type); + if (type === 'VPPBridge') { + let fw = me.down('field[name=firewall]'); + if (fw) { + fw.setValue(false); + } + } + }, + }, }); me.column1 = [ @@ -96,6 +112,17 @@ Ext.define('PVE.qemu.NetworkInputPanel', { fieldLabel: gettext('Firewall'), name: 'firewall', checked: me.insideWizard || me.isCreate, + bind: { + disabled: '{isVPPBridge}', + }, + }, + { + xtype: 'displayfield', + userCls: 'pmx-hint', + value: gettext('Kernel firewall is not available with VPP bridges'), + bind: { + hidden: '{!isVPPBridge}', + }, }, ]; diff --git a/www/manager6/window/Migrate.js b/www/manager6/window/Migrate.js index ff80c70c..c1509be6 100644 --- a/www/manager6/window/Migrate.js +++ b/www/manager6/window/Migrate.js @@ -463,6 +463,54 @@ Ext.define('PVE.window.Migrate', { } } + if (vm.get('running')) { + try { + let { result: netResult } = await Proxmox.Async.api2({ + url: `/nodes/${vm.get('nodename')}/network?type=any_bridge`, + method: 'GET', + }); + let vppBridges = new Set(); + for (const iface of netResult.data || []) { + if (iface.type === 'VPPBridge') { + vppBridges.add(iface.iface); + } + } + if (vppBridges.size > 0) { + let vmConfig = {}; + try { + let { result: cfgResult } = await Proxmox.Async.api2({ + url: `/nodes/${vm.get('nodename')}/qemu/${vm.get('vmid')}/config`, + method: 'GET', + }); + vmConfig = cfgResult.data || {}; + } catch (_err) { /* ignore */ } + + let vppNics = []; + for (const [key, value] of Object.entries(vmConfig)) { + if (!key.match(/^net\d+$/)) { + continue; + } + let net = PVE.Parser.parseQemuNetwork(key, value); + if (net && net.bridge && vppBridges.has(net.bridge)) { + vppNics.push(key); + } + } + if (vppNics.length > 0) { + migration.possible = false; + migration.preconditions.push({ + text: Ext.String.format( + gettext('Cannot live-migrate VM with VPP vhost-user NICs: {0}. Use offline migration or HA (stop/start).'), + vppNics.join(', '), + ), + severity: 'error', + }); + } + } + } catch (_err) { + // VPP bridge check is best-effort + } + } + vm.set('migration', migration); }, checkLxcPreconditions: async function (resetMigrationPossible) { -- 2.50.1 (Apple Git-155)
