From: Fiona Ebner <f.ebner@proxmox.com>
To: pve-devel@lists.proxmox.com
Subject: [pve-devel] [PATCH qemu-server 08/31] introduce Network module
Date: Wed, 25 Jun 2025 17:56:31 +0200 [thread overview]
Message-ID: <20250625155751.268047-9-f.ebner@proxmox.com> (raw)
In-Reply-To: <20250625155751.268047-1-f.ebner@proxmox.com>
Also gets rid of a cyclic dependency between the main QemuServer
module and the Cloudinit module.
Signed-off-by: Fiona Ebner <f.ebner@proxmox.com>
---
src/PVE/API2/Qemu.pm | 18 +-
src/PVE/QemuMigrate.pm | 7 +-
src/PVE/QemuServer.pm | 347 ++--------------------
src/PVE/QemuServer/Cloudinit.pm | 18 +-
src/PVE/QemuServer/Makefile | 1 +
src/PVE/QemuServer/Network.pm | 324 ++++++++++++++++++++
src/test/MigrationTest/QemuMigrateMock.pm | 6 +-
src/usr/pve-bridge | 5 +-
8 files changed, 375 insertions(+), 351 deletions(-)
create mode 100644 src/PVE/QemuServer/Network.pm
diff --git a/src/PVE/API2/Qemu.pm b/src/PVE/API2/Qemu.pm
index 6830ea1e..9600cf8d 100644
--- a/src/PVE/API2/Qemu.pm
+++ b/src/PVE/API2/Qemu.pm
@@ -36,6 +36,7 @@ use PVE::QemuServer::Monitor qw(mon_cmd);
use PVE::QemuServer::Machine;
use PVE::QemuServer::Memory qw(get_current_memory);
use PVE::QemuServer::MetaInfo;
+use PVE::QemuServer::Network;
use PVE::QemuServer::OVMF;
use PVE::QemuServer::PCI;
use PVE::QemuServer::QMPHelpers;
@@ -1277,7 +1278,7 @@ __PACKAGE__->register_method({
$check_drive_param->($param, $storecfg);
- PVE::QemuServer::add_random_macs($param);
+ PVE::QemuServer::Network::add_random_macs($param);
}
my $emsg = $is_restore ? "unable to restore VM $vmid -" : "unable to create VM $vmid -";
@@ -1354,7 +1355,8 @@ __PACKAGE__->register_method({
warn $@ if $@;
}
- PVE::QemuServer::create_ifaces_ipams_ips($restored_conf, $vmid) if $unique;
+ PVE::QemuServer::Network::create_ifaces_ipams_ips($restored_conf, $vmid)
+ if $unique;
};
# ensure no old replication state are exists
@@ -1445,7 +1447,7 @@ __PACKAGE__->register_method({
PVE::AccessControl::add_vm_to_pool($vmid, $pool) if $pool;
- PVE::QemuServer::create_ifaces_ipams_ips($conf, $vmid);
+ PVE::QemuServer::Network::create_ifaces_ipams_ips($conf, $vmid);
};
PVE::QemuConfig->lock_config_full($vmid, 1, $realcmd);
@@ -2062,8 +2064,8 @@ my $update_vm_api = sub {
foreach my $opt (keys %$param) {
if ($opt =~ m/^net(\d+)$/) {
# add macaddr
- my $net = PVE::QemuServer::parse_net($param->{$opt});
- $param->{$opt} = PVE::QemuServer::print_net($net);
+ my $net = PVE::QemuServer::Network::parse_net($param->{$opt});
+ $param->{$opt} = PVE::QemuServer::Network::print_net($net);
} elsif ($opt eq 'vmgenid') {
if ($param->{$opt} eq '1') {
$param->{$opt} = PVE::QemuServer::generate_uuid();
@@ -4332,10 +4334,10 @@ __PACKAGE__->register_method({
# always change MAC! address
if ($opt =~ m/^net(\d+)$/) {
- my $net = PVE::QemuServer::parse_net($value);
+ my $net = PVE::QemuServer::Network::parse_net($value);
my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
$net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
- $newconf->{$opt} = PVE::QemuServer::print_net($net);
+ $newconf->{$opt} = PVE::QemuServer::Network::print_net($net);
} elsif (PVE::QemuServer::is_valid_drivename($opt)) {
my $drive = PVE::QemuServer::parse_drive($opt, $value);
die "unable to parse drive options for '$opt'\n" if !$drive;
@@ -4488,7 +4490,7 @@ __PACKAGE__->register_method({
PVE::QemuConfig->write_config($newid, $newconf);
- PVE::QemuServer::create_ifaces_ipams_ips($newconf, $newid);
+ PVE::QemuServer::Network::create_ifaces_ipams_ips($newconf, $newid);
if ($target) {
if (!$running) {
diff --git a/src/PVE/QemuMigrate.pm b/src/PVE/QemuMigrate.pm
index 28d7ac56..934d4350 100644
--- a/src/PVE/QemuMigrate.pm
+++ b/src/PVE/QemuMigrate.pm
@@ -31,6 +31,7 @@ use PVE::QemuServer::Helpers qw(min_version);
use PVE::QemuServer::Machine;
use PVE::QemuServer::Monitor qw(mon_cmd);
use PVE::QemuServer::Memory qw(get_current_memory);
+use PVE::QemuServer::Network;
use PVE::QemuServer::QMPHelpers;
use PVE::QemuServer;
@@ -809,7 +810,7 @@ sub map_bridges {
next if $opt !~ m/^net\d+$/;
next if !$conf->{$opt};
- my $d = PVE::QemuServer::parse_net($conf->{$opt});
+ my $d = PVE::QemuServer::Network::parse_net($conf->{$opt});
next if !$d || !$d->{bridge};
my $target_bridge = PVE::JSONSchema::map_id($map, $d->{bridge});
@@ -818,7 +819,7 @@ sub map_bridges {
next if $scan_only;
$d->{bridge} = $target_bridge;
- $conf->{$opt} = PVE::QemuServer::print_net($d);
+ $conf->{$opt} = PVE::QemuServer::Network::print_net($d);
}
return $bridges;
@@ -1623,7 +1624,7 @@ sub phase3_cleanup {
}
# deletes local FDB entries if learning is disabled, they'll be re-added on target on resume
- PVE::QemuServer::del_nets_bridge_fdb($conf, $vmid);
+ PVE::QemuServer::Network::del_nets_bridge_fdb($conf, $vmid);
if (!$self->{vm_was_paused}) {
# config moved and nbd server stopped - now we can resume vm on target
diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
index 2335703b..59958dc0 100644
--- a/src/PVE/QemuServer.pm
+++ b/src/PVE/QemuServer.pm
@@ -73,6 +73,7 @@ use PVE::QemuServer::Machine;
use PVE::QemuServer::Memory qw(get_current_memory);
use PVE::QemuServer::MetaInfo;
use PVE::QemuServer::Monitor qw(mon_cmd);
+use PVE::QemuServer::Network;
use PVE::QemuServer::OVMF;
use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_root_port parse_hostpci);
use PVE::QemuServer::QemuImage;
@@ -855,180 +856,13 @@ for (my $i = 0; $i < $PVE::QemuServer::Memory::MAX_NUMA; $i++) {
$confdesc->{"numa$i"} = $PVE::QemuServer::Memory::numadesc;
}
-my $nic_model_list = [
- 'e1000',
- 'e1000-82540em',
- 'e1000-82544gc',
- 'e1000-82545em',
- 'e1000e',
- 'i82551',
- 'i82557b',
- 'i82559er',
- 'ne2k_isa',
- 'ne2k_pci',
- 'pcnet',
- 'rtl8139',
- 'virtio',
- 'vmxnet3',
-];
-
-my $net_fmt_bridge_descr = <<__EOD__;
-Bridge to attach the network device to. The Proxmox VE standard bridge
-is called 'vmbr0'.
-
-If you do not specify a bridge, we create a kvm user (NATed) network
-device, which provides DHCP and DNS services. The following addresses
-are used:
-
- 10.0.2.2 Gateway
- 10.0.2.3 DNS Server
- 10.0.2.4 SMB Server
-
-The DHCP server assign addresses to the guest starting from 10.0.2.15.
-__EOD__
-
-my $net_fmt = {
- macaddr => get_standard_option(
- 'mac-addr',
- {
- description =>
- "MAC address. That address must be unique within your network. This is"
- . " automatically generated if not specified.",
- },
- ),
- model => {
- type => 'string',
- description =>
- "Network Card Model. The 'virtio' model provides the best performance with"
- . " very low CPU overhead. If your guest does not support this driver, it is usually"
- . " best to use 'e1000'.",
- enum => $nic_model_list,
- default_key => 1,
- },
- (map { $_ => { keyAlias => 'model', alias => 'macaddr' } } @$nic_model_list),
- bridge => get_standard_option(
- 'pve-bridge-id',
- {
- description => $net_fmt_bridge_descr,
- optional => 1,
- },
- ),
- queues => {
- type => 'integer',
- minimum => 0,
- maximum => 64,
- description => 'Number of packet queues to be used on the device.',
- optional => 1,
- },
- rate => {
- type => 'number',
- minimum => 0,
- description => "Rate limit in mbps (megabytes per second) as floating point number.",
- optional => 1,
- },
- tag => {
- type => 'integer',
- minimum => 1,
- maximum => 4094,
- description => 'VLAN tag to apply to packets on this interface.',
- optional => 1,
- },
- trunks => {
- type => 'string',
- pattern => qr/\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*/,
- description => 'VLAN trunks to pass through this interface.',
- format_description => 'vlanid[;vlanid...]',
- optional => 1,
- },
- firewall => {
- type => 'boolean',
- description => 'Whether this interface should be protected by the firewall.',
- optional => 1,
- },
- link_down => {
- type => 'boolean',
- description => 'Whether this interface should be disconnected (like pulling the plug).',
- optional => 1,
- },
- mtu => {
- type => 'integer',
- minimum => 1,
- maximum => 65520,
- description => "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU",
- optional => 1,
- },
-};
-
-my $netdesc = {
- optional => 1,
- type => 'string',
- format => $net_fmt,
- description => "Specify network devices.",
-};
-
-PVE::JSONSchema::register_standard_option("pve-qm-net", $netdesc);
-
for (my $i = 0; $i < max_virtiofs(); $i++) {
$confdesc->{"virtiofs$i"} = get_standard_option('pve-qm-virtiofs');
}
-my $ipconfig_fmt = {
- ip => {
- type => 'string',
- format => 'pve-ipv4-config',
- format_description => 'IPv4Format/CIDR',
- description => 'IPv4 address in CIDR format.',
- optional => 1,
- default => 'dhcp',
- },
- gw => {
- type => 'string',
- format => 'ipv4',
- format_description => 'GatewayIPv4',
- description => 'Default gateway for IPv4 traffic.',
- optional => 1,
- requires => 'ip',
- },
- ip6 => {
- type => 'string',
- format => 'pve-ipv6-config',
- format_description => 'IPv6Format/CIDR',
- description => 'IPv6 address in CIDR format.',
- optional => 1,
- default => 'dhcp',
- },
- gw6 => {
- type => 'string',
- format => 'ipv6',
- format_description => 'GatewayIPv6',
- description => 'Default gateway for IPv6 traffic.',
- optional => 1,
- requires => 'ip6',
- },
-};
-PVE::JSONSchema::register_format('pve-qm-ipconfig', $ipconfig_fmt);
-my $ipconfigdesc = {
- optional => 1,
- type => 'string',
- format => 'pve-qm-ipconfig',
- description => <<'EODESCR',
-cloud-init: Specify IP addresses and gateways for the corresponding interface.
-
-IP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.
-
-The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit
-gateway should be provided.
-For IPv6 the special string 'auto' can be used to use stateless autoconfiguration. This requires
-cloud-init 19.4 or newer.
-
-If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using
-dhcp on IPv4.
-EODESCR
-};
-
for (my $i = 0; $i < $MAX_NETS; $i++) {
- $confdesc->{"net$i"} = $netdesc;
- $confdesc_cloudinit->{"ipconfig$i"} = $ipconfigdesc;
+ $confdesc->{"net$i"} = $PVE::QemuServer::Network::netdesc;
+ $confdesc_cloudinit->{"ipconfig$i"} = $PVE::QemuServer::Network::ipconfigdesc;
}
foreach my $key (keys %$confdesc_cloudinit) {
@@ -1755,74 +1589,6 @@ sub print_vga_device {
return "$type,id=${vgaid}${memory}${max_outputs}${pciaddr}${edidoff}";
}
-# netX: e1000=XX:XX:XX:XX:XX:XX,bridge=vmbr0,rate=<mbps>
-sub parse_net {
- my ($data, $disable_mac_autogen) = @_;
-
- my $res = eval { parse_property_string($net_fmt, $data) };
- if ($@) {
- warn $@;
- return;
- }
- if (!defined($res->{macaddr}) && !$disable_mac_autogen) {
- my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
- $res->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
- }
- return $res;
-}
-
-# ipconfigX ip=cidr,gw=ip,ip6=cidr,gw6=ip
-sub parse_ipconfig {
- my ($data) = @_;
-
- my $res = eval { parse_property_string($ipconfig_fmt, $data) };
- if ($@) {
- warn $@;
- return;
- }
-
- if ($res->{gw} && !$res->{ip}) {
- warn 'gateway specified without specifying an IP address';
- return;
- }
- if ($res->{gw6} && !$res->{ip6}) {
- warn 'IPv6 gateway specified without specifying an IPv6 address';
- return;
- }
- if ($res->{gw} && $res->{ip} eq 'dhcp') {
- warn 'gateway specified together with DHCP';
- return;
- }
- if ($res->{gw6} && $res->{ip6} !~ /^$IPV6RE/) {
- # gw6 + auto/dhcp
- warn "IPv6 gateway specified together with $res->{ip6} address";
- return;
- }
-
- if (!$res->{ip} && !$res->{ip6}) {
- return { ip => 'dhcp', ip6 => 'dhcp' };
- }
-
- return $res;
-}
-
-sub print_net {
- my $net = shift;
-
- return PVE::JSONSchema::print_property_string($net, $net_fmt);
-}
-
-sub add_random_macs {
- my ($settings) = @_;
-
- foreach my $opt (keys %$settings) {
- next if $opt !~ m/^net(\d+)$/;
- my $net = parse_net($settings->{$opt});
- next if !$net;
- $settings->{$opt} = print_net($net);
- }
-}
-
sub vm_is_volid_owner {
my ($storecfg, $vmid, $volid) = @_;
@@ -2179,7 +1945,7 @@ sub destroy_vm {
);
}
- eval { delete_ifaces_ipams_ips($conf, $vmid) };
+ eval { PVE::QemuServer::Network::delete_ifaces_ipams_ips($conf, $vmid) };
warn $@ if $@;
if (defined $replacement_conf) {
@@ -3979,7 +3745,7 @@ sub config_to_command {
my $netname = "net$i";
next if !$conf->{$netname};
- my $d = parse_net($conf->{$netname});
+ my $d = PVE::QemuServer::Network::parse_net($conf->{$netname});
next if !$d;
# save the MAC addr here (could be auto-gen. in some odd setups) for FDB registering later?
@@ -5004,7 +4770,7 @@ sub vmconfig_hotplug_pending {
} elsif ($opt =~ m/^net(\d+)$/) {
die "skip\n" if !$hotplug_features->{network};
vm_deviceunplug($vmid, $conf, $opt);
- my $net = PVE::QemuServer::parse_net($conf->{$opt});
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$opt});
PVE::Network::SDN::Vnets::del_ips_from_mac(
$net->{bridge},
$net->{macaddr},
@@ -5243,7 +5009,7 @@ sub vmconfig_apply_pending {
} elsif (defined($conf->{$opt}) && is_valid_drivename($opt)) {
vmconfig_delete_or_detach_drive($vmid, $storecfg, $conf, $opt, $force);
} elsif (defined($conf->{$opt}) && $opt =~ m/^net\d+$/) {
- my $net = PVE::QemuServer::parse_net($conf->{$opt});
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$opt});
eval {
PVE::Network::SDN::Vnets::del_ips_from_mac(
$net->{bridge},
@@ -5277,9 +5043,9 @@ sub vmconfig_apply_pending {
parse_drive($opt, $conf->{$opt}),
);
} elsif (defined($conf->{pending}->{$opt}) && $opt =~ m/^net\d+$/) {
- my $new_net = PVE::QemuServer::parse_net($conf->{pending}->{$opt});
+ my $new_net = PVE::QemuServer::Network::parse_net($conf->{pending}->{$opt});
if ($conf->{$opt}) {
- my $old_net = PVE::QemuServer::parse_net($conf->{$opt});
+ my $old_net = PVE::QemuServer::Network::parse_net($conf->{$opt});
if (
defined($old_net->{bridge})
@@ -5340,10 +5106,10 @@ sub vmconfig_apply_pending {
sub vmconfig_update_net {
my ($storecfg, $conf, $hotplug, $vmid, $opt, $value, $arch, $machine_type) = @_;
- my $newnet = parse_net($value);
+ my $newnet = PVE::QemuServer::Network::parse_net($value);
if ($conf->{$opt}) {
- my $oldnet = parse_net($conf->{$opt});
+ my $oldnet = PVE::QemuServer::Network::parse_net($conf->{$opt});
if (
safe_string_ne($oldnet->{model}, $newnet->{model})
@@ -6148,10 +5914,10 @@ sub vm_start_nolock {
foreach my $opt (keys %$conf) {
next if $opt !~ m/^net\d+$/;
- my $nicconf = parse_net($conf->{$opt});
+ my $nicconf = PVE::QemuServer::Network::parse_net($conf->{$opt});
qemu_set_link_status($vmid, $opt, 0) if $nicconf->{link_down};
}
- add_nets_bridge_fdb($conf, $vmid);
+ PVE::QemuServer::Network::add_nets_bridge_fdb($conf, $vmid);
}
if (!defined($conf->{balloon}) || $conf->{balloon}) {
@@ -6686,7 +6452,8 @@ sub vm_resume {
mon_cmd($vmid, "system_reset");
}
- add_nets_bridge_fdb($conf, $vmid) if $resume_cmd eq 'cont';
+ PVE::QemuServer::Network::add_nets_bridge_fdb($conf, $vmid)
+ if $resume_cmd eq 'cont';
mon_cmd($vmid, $resume_cmd);
},
@@ -6716,7 +6483,7 @@ sub check_bridge_access {
for my $opt (sort keys $conf->%*) {
next if $opt !~ m/^net\d+$/;
- my $net = parse_net($conf->{$opt});
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$opt});
my ($bridge, $tag, $trunks) = $net->@{ 'bridge', 'tag', 'trunks' };
PVE::GuestHelpers::check_vnet_access($rpcenv, $authuser, $bridge, $tag, $trunks);
}
@@ -7017,16 +6784,16 @@ sub restore_update_config_line {
bridge => "vmbr$ind",
macaddr => $macaddr,
};
- my $netstr = print_net($net);
+ my $netstr = PVE::QemuServer::Network::print_net($net);
$res .= "net$cookie->{netcount}: $netstr\n";
$cookie->{netcount}++;
}
} elsif (($line =~ m/^(net\d+):\s*(\S+)\s*$/) && $unique) {
my ($id, $netstr) = ($1, $2);
- my $net = parse_net($netstr);
+ my $net = PVE::QemuServer::Network::parse_net($netstr);
$net->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix}) if $net->{macaddr};
- $netstr = print_net($net);
+ $netstr = PVE::QemuServer::Network::print_net($net);
$res .= "$id: $netstr\n";
} elsif ($line =~ m/^((ide|scsi|virtio|sata|efidisk|tpmstate)\d+):\s*(\S+)\s*$/) {
my $virtdev = $1;
@@ -9094,80 +8861,4 @@ sub check_volume_storage_type {
return 1;
}
-sub add_nets_bridge_fdb {
- my ($conf, $vmid) = @_;
-
- for my $opt (keys %$conf) {
- next if $opt !~ m/^net(\d+)$/;
- my $iface = "tap${vmid}i$1";
- # NOTE: expect setups with learning off to *not* use auto-random-generation of MAC on start
- my $net = parse_net($conf->{$opt}, 1) or next;
-
- my $mac = $net->{macaddr};
- if (!$mac) {
- log_warn(
- "MAC learning disabled, but vNIC '$iface' has no static MAC to add to forwarding DB!"
- ) if !file_read_firstline("/sys/class/net/$iface/brport/learning");
- next;
- }
-
- my $bridge = $net->{bridge};
- if (!$bridge) {
- log_warn("Interface '$iface' not attached to any bridge.");
- next;
- }
- PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $bridge);
- }
-}
-
-sub del_nets_bridge_fdb {
- my ($conf, $vmid) = @_;
-
- for my $opt (keys %$conf) {
- next if $opt !~ m/^net(\d+)$/;
- my $iface = "tap${vmid}i$1";
-
- my $net = parse_net($conf->{$opt}) or next;
- my $mac = $net->{macaddr} or next;
-
- my $bridge = $net->{bridge};
- PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $bridge);
- }
-}
-
-sub create_ifaces_ipams_ips {
- my ($conf, $vmid) = @_;
-
- foreach my $opt (keys %$conf) {
- if ($opt =~ m/^net(\d+)$/) {
- my $value = $conf->{$opt};
- my $net = PVE::QemuServer::parse_net($value);
- eval {
- PVE::Network::SDN::Vnets::add_next_free_cidr(
- $net->{bridge}, $conf->{name}, $net->{macaddr}, $vmid, undef, 1,
- );
- };
- warn $@ if $@;
- }
- }
-}
-
-sub delete_ifaces_ipams_ips {
- my ($conf, $vmid) = @_;
-
- foreach my $opt (keys %$conf) {
- if ($opt =~ m/^net(\d+)$/) {
- my $net = PVE::QemuServer::parse_net($conf->{$opt});
- eval {
- PVE::Network::SDN::Vnets::del_ips_from_mac(
- $net->{bridge},
- $net->{macaddr},
- $conf->{name},
- );
- };
- warn $@ if $@;
- }
- }
-}
-
1;
diff --git a/src/PVE/QemuServer/Cloudinit.pm b/src/PVE/QemuServer/Cloudinit.pm
index 0d04e98f..349cf90b 100644
--- a/src/PVE/QemuServer/Cloudinit.pm
+++ b/src/PVE/QemuServer/Cloudinit.pm
@@ -12,9 +12,9 @@ use JSON;
use PVE::Tools qw(run_command file_set_contents);
use PVE::Storage;
-use PVE::QemuServer;
use PVE::QemuServer::Drive qw(checked_volume_format);
use PVE::QemuServer::Helpers;
+use PVE::QemuServer::Network;
use constant CLOUDINIT_DISK_SIZE => 4 * 1024 * 1024; # 4MiB in bytes
@@ -191,7 +191,7 @@ sub configdrive2_network {
foreach my $iface (sort @ifaces) {
(my $id = $iface) =~ s/^net//;
next if !$conf->{"ipconfig$id"};
- my $net = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+ my $net = PVE::QemuServer::Network::parse_ipconfig($conf->{"ipconfig$id"});
$id = "eth$id";
$content .= "auto $id\n";
@@ -291,7 +291,7 @@ sub cloudbase_network_eni {
foreach my $iface (sort @ifaces) {
(my $id = $iface) =~ s/^net//;
next if !$conf->{"ipconfig$id"};
- my $net = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+ my $net = PVE::QemuServer::Network::parse_ipconfig($conf->{"ipconfig$id"});
$id = "eth$id";
$content .= "auto $id\n";
@@ -383,9 +383,9 @@ sub generate_opennebula {
my @ifaces = grep { /^net(\d+)$/ } keys %$conf;
foreach my $iface (sort @ifaces) {
(my $id = $iface) =~ s/^net//;
- my $net = PVE::QemuServer::parse_net($conf->{$iface});
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$iface});
next if !$conf->{"ipconfig$id"};
- my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+ my $ipconfig = PVE::QemuServer::Network::parse_ipconfig($conf->{"ipconfig$id"});
my $ethid = "ETH$id";
my $mac = lc $net->{hwaddr};
@@ -445,8 +445,8 @@ sub nocloud_network_v2 {
# indentation - network interfaces are inside an 'ethernets' hash
my $i = ' ';
- my $net = PVE::QemuServer::parse_net($conf->{$iface});
- my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$iface});
+ my $ipconfig = PVE::QemuServer::Network::parse_ipconfig($conf->{"ipconfig$id"});
my $mac = $net->{macaddr}
or die "network interface '$iface' has no mac address\n";
@@ -513,8 +513,8 @@ sub nocloud_network {
# indentation - network interfaces are inside an 'ethernets' hash
my $i = ' ';
- my $net = PVE::QemuServer::parse_net($conf->{$iface});
- my $ipconfig = PVE::QemuServer::parse_ipconfig($conf->{"ipconfig$id"});
+ my $net = PVE::QemuServer::Network::parse_net($conf->{$iface});
+ my $ipconfig = PVE::QemuServer::Network::parse_ipconfig($conf->{"ipconfig$id"});
my $mac = lc($net->{macaddr})
or die "network interface '$iface' has no mac address\n";
diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
index dd6fe505..e30c571c 100644
--- a/src/PVE/QemuServer/Makefile
+++ b/src/PVE/QemuServer/Makefile
@@ -14,6 +14,7 @@ SOURCES=Agent.pm \
Memory.pm \
MetaInfo.pm \
Monitor.pm \
+ Network.pm \
OVMF.pm \
PCI.pm \
QemuImage.pm \
diff --git a/src/PVE/QemuServer/Network.pm b/src/PVE/QemuServer/Network.pm
new file mode 100644
index 00000000..84d8981a
--- /dev/null
+++ b/src/PVE/QemuServer/Network.pm
@@ -0,0 +1,324 @@
+package PVE::QemuServer::Network;
+
+use strict;
+use warnings;
+
+use PVE::Cluster;
+use PVE::JSONSchema qw(get_standard_option parse_property_string);
+use PVE::Network::SDN::Vnets;
+use PVE::Network::SDN::Zones;
+use PVE::RESTEnvironment qw(log_warn);
+use PVE::Tools qw($IPV6RE file_read_firstline);
+
+my $nic_model_list = [
+ 'e1000',
+ 'e1000-82540em',
+ 'e1000-82544gc',
+ 'e1000-82545em',
+ 'e1000e',
+ 'i82551',
+ 'i82557b',
+ 'i82559er',
+ 'ne2k_isa',
+ 'ne2k_pci',
+ 'pcnet',
+ 'rtl8139',
+ 'virtio',
+ 'vmxnet3',
+];
+
+my $net_fmt_bridge_descr = <<__EOD__;
+Bridge to attach the network device to. The Proxmox VE standard bridge
+is called 'vmbr0'.
+
+If you do not specify a bridge, we create a kvm user (NATed) network
+device, which provides DHCP and DNS services. The following addresses
+are used:
+
+ 10.0.2.2 Gateway
+ 10.0.2.3 DNS Server
+ 10.0.2.4 SMB Server
+
+The DHCP server assign addresses to the guest starting from 10.0.2.15.
+__EOD__
+
+my $net_fmt = {
+ macaddr => get_standard_option(
+ 'mac-addr',
+ {
+ description =>
+ "MAC address. That address must be unique within your network. This is"
+ . " automatically generated if not specified.",
+ },
+ ),
+ model => {
+ type => 'string',
+ description =>
+ "Network Card Model. The 'virtio' model provides the best performance with"
+ . " very low CPU overhead. If your guest does not support this driver, it is usually"
+ . " best to use 'e1000'.",
+ enum => $nic_model_list,
+ default_key => 1,
+ },
+ (map { $_ => { keyAlias => 'model', alias => 'macaddr' } } @$nic_model_list),
+ bridge => get_standard_option(
+ 'pve-bridge-id',
+ {
+ description => $net_fmt_bridge_descr,
+ optional => 1,
+ },
+ ),
+ queues => {
+ type => 'integer',
+ minimum => 0,
+ maximum => 64,
+ description => 'Number of packet queues to be used on the device.',
+ optional => 1,
+ },
+ rate => {
+ type => 'number',
+ minimum => 0,
+ description => "Rate limit in mbps (megabytes per second) as floating point number.",
+ optional => 1,
+ },
+ tag => {
+ type => 'integer',
+ minimum => 1,
+ maximum => 4094,
+ description => 'VLAN tag to apply to packets on this interface.',
+ optional => 1,
+ },
+ trunks => {
+ type => 'string',
+ pattern => qr/\d+(?:-\d+)?(?:;\d+(?:-\d+)?)*/,
+ description => 'VLAN trunks to pass through this interface.',
+ format_description => 'vlanid[;vlanid...]',
+ optional => 1,
+ },
+ firewall => {
+ type => 'boolean',
+ description => 'Whether this interface should be protected by the firewall.',
+ optional => 1,
+ },
+ link_down => {
+ type => 'boolean',
+ description => 'Whether this interface should be disconnected (like pulling the plug).',
+ optional => 1,
+ },
+ mtu => {
+ type => 'integer',
+ minimum => 1,
+ maximum => 65520,
+ description => "Force MTU, for VirtIO only. Set to '1' to use the bridge MTU",
+ optional => 1,
+ },
+};
+
+our $netdesc = {
+ optional => 1,
+ type => 'string',
+ format => $net_fmt,
+ description => "Specify network devices.",
+};
+
+PVE::JSONSchema::register_standard_option("pve-qm-net", $netdesc);
+
+my $ipconfig_fmt = {
+ ip => {
+ type => 'string',
+ format => 'pve-ipv4-config',
+ format_description => 'IPv4Format/CIDR',
+ description => 'IPv4 address in CIDR format.',
+ optional => 1,
+ default => 'dhcp',
+ },
+ gw => {
+ type => 'string',
+ format => 'ipv4',
+ format_description => 'GatewayIPv4',
+ description => 'Default gateway for IPv4 traffic.',
+ optional => 1,
+ requires => 'ip',
+ },
+ ip6 => {
+ type => 'string',
+ format => 'pve-ipv6-config',
+ format_description => 'IPv6Format/CIDR',
+ description => 'IPv6 address in CIDR format.',
+ optional => 1,
+ default => 'dhcp',
+ },
+ gw6 => {
+ type => 'string',
+ format => 'ipv6',
+ format_description => 'GatewayIPv6',
+ description => 'Default gateway for IPv6 traffic.',
+ optional => 1,
+ requires => 'ip6',
+ },
+};
+PVE::JSONSchema::register_format('pve-qm-ipconfig', $ipconfig_fmt);
+our $ipconfigdesc = {
+ optional => 1,
+ type => 'string',
+ format => 'pve-qm-ipconfig',
+ description => <<'EODESCR',
+cloud-init: Specify IP addresses and gateways for the corresponding interface.
+
+IP addresses use CIDR notation, gateways are optional but need an IP of the same type specified.
+
+The special string 'dhcp' can be used for IP addresses to use DHCP, in which case no explicit
+gateway should be provided.
+For IPv6 the special string 'auto' can be used to use stateless autoconfiguration. This requires
+cloud-init 19.4 or newer.
+
+If cloud-init is enabled and neither an IPv4 nor an IPv6 address is specified, it defaults to using
+dhcp on IPv4.
+EODESCR
+};
+
+# netX: e1000=XX:XX:XX:XX:XX:XX,bridge=vmbr0,rate=<mbps>
+sub parse_net {
+ my ($data, $disable_mac_autogen) = @_;
+
+ my $res = eval { parse_property_string($net_fmt, $data) };
+ if ($@) {
+ warn $@;
+ return;
+ }
+ if (!defined($res->{macaddr}) && !$disable_mac_autogen) {
+ my $dc = PVE::Cluster::cfs_read_file('datacenter.cfg');
+ $res->{macaddr} = PVE::Tools::random_ether_addr($dc->{mac_prefix});
+ }
+ return $res;
+}
+
+# ipconfigX ip=cidr,gw=ip,ip6=cidr,gw6=ip
+sub parse_ipconfig {
+ my ($data) = @_;
+
+ my $res = eval { parse_property_string($ipconfig_fmt, $data) };
+ if ($@) {
+ warn $@;
+ return;
+ }
+
+ if ($res->{gw} && !$res->{ip}) {
+ warn 'gateway specified without specifying an IP address';
+ return;
+ }
+ if ($res->{gw6} && !$res->{ip6}) {
+ warn 'IPv6 gateway specified without specifying an IPv6 address';
+ return;
+ }
+ if ($res->{gw} && $res->{ip} eq 'dhcp') {
+ warn 'gateway specified together with DHCP';
+ return;
+ }
+ if ($res->{gw6} && $res->{ip6} !~ /^$IPV6RE/) {
+ # gw6 + auto/dhcp
+ warn "IPv6 gateway specified together with $res->{ip6} address";
+ return;
+ }
+
+ if (!$res->{ip} && !$res->{ip6}) {
+ return { ip => 'dhcp', ip6 => 'dhcp' };
+ }
+
+ return $res;
+}
+
+sub print_net {
+ my $net = shift;
+
+ return PVE::JSONSchema::print_property_string($net, $net_fmt);
+}
+
+sub add_random_macs {
+ my ($settings) = @_;
+
+ foreach my $opt (keys %$settings) {
+ next if $opt !~ m/^net(\d+)$/;
+ my $net = parse_net($settings->{$opt});
+ next if !$net;
+ $settings->{$opt} = print_net($net);
+ }
+}
+
+sub add_nets_bridge_fdb {
+ my ($conf, $vmid) = @_;
+
+ for my $opt (keys %$conf) {
+ next if $opt !~ m/^net(\d+)$/;
+ my $iface = "tap${vmid}i$1";
+ # NOTE: expect setups with learning off to *not* use auto-random-generation of MAC on start
+ my $net = parse_net($conf->{$opt}, 1) or next;
+
+ my $mac = $net->{macaddr};
+ if (!$mac) {
+ log_warn(
+ "MAC learning disabled, but vNIC '$iface' has no static MAC to add to forwarding DB!"
+ ) if !file_read_firstline("/sys/class/net/$iface/brport/learning");
+ next;
+ }
+
+ my $bridge = $net->{bridge};
+ if (!$bridge) {
+ log_warn("Interface '$iface' not attached to any bridge.");
+ next;
+ }
+ PVE::Network::SDN::Zones::add_bridge_fdb($iface, $mac, $bridge);
+ }
+}
+
+sub del_nets_bridge_fdb {
+ my ($conf, $vmid) = @_;
+
+ for my $opt (keys %$conf) {
+ next if $opt !~ m/^net(\d+)$/;
+ my $iface = "tap${vmid}i$1";
+
+ my $net = parse_net($conf->{$opt}) or next;
+ my $mac = $net->{macaddr} or next;
+
+ my $bridge = $net->{bridge};
+ PVE::Network::SDN::Zones::del_bridge_fdb($iface, $mac, $bridge);
+ }
+}
+
+sub create_ifaces_ipams_ips {
+ my ($conf, $vmid) = @_;
+
+ foreach my $opt (keys %$conf) {
+ if ($opt =~ m/^net(\d+)$/) {
+ my $value = $conf->{$opt};
+ my $net = parse_net($value);
+ eval {
+ PVE::Network::SDN::Vnets::add_next_free_cidr(
+ $net->{bridge}, $conf->{name}, $net->{macaddr}, $vmid, undef, 1,
+ );
+ };
+ warn $@ if $@;
+ }
+ }
+}
+
+sub delete_ifaces_ipams_ips {
+ my ($conf, $vmid) = @_;
+
+ foreach my $opt (keys %$conf) {
+ if ($opt =~ m/^net(\d+)$/) {
+ my $net = parse_net($conf->{$opt});
+ eval {
+ PVE::Network::SDN::Vnets::del_ips_from_mac(
+ $net->{bridge},
+ $net->{macaddr},
+ $conf->{name},
+ );
+ };
+ warn $@ if $@;
+ }
+ }
+}
+
+1;
diff --git a/src/test/MigrationTest/QemuMigrateMock.pm b/src/test/MigrationTest/QemuMigrateMock.pm
index f678f9ec..1b95a2ff 100644
--- a/src/test/MigrationTest/QemuMigrateMock.pm
+++ b/src/test/MigrationTest/QemuMigrateMock.pm
@@ -174,7 +174,6 @@ $MigrationTest::Shared::qemu_server_module->mock(
$vm_stop_executed = 1;
delete $expected_calls->{'vm_stop'};
},
- del_nets_bridge_fdb => sub { return; },
);
my $qemu_server_cpuconfig_module = Test::MockModule->new("PVE::QemuServer::CPUConfig");
@@ -203,6 +202,11 @@ $qemu_server_machine_module->mock(
},
);
+my $qemu_server_network_module = Test::MockModule->new("PVE::QemuServer::Network");
+$qemu_server_network_module->mock(
+ del_nets_bridge_fdb => sub { return; },
+);
+
my $qemu_server_qmphelpers_module = Test::MockModule->new("PVE::QemuServer::QMPHelpers");
$qemu_server_qmphelpers_module->mock(
runs_at_least_qemu_version => sub {
diff --git a/src/usr/pve-bridge b/src/usr/pve-bridge
index 2608e1a0..2f529364 100755
--- a/src/usr/pve-bridge
+++ b/src/usr/pve-bridge
@@ -3,12 +3,13 @@
use strict;
use warnings;
-use PVE::QemuServer;
use PVE::Tools qw(run_command);
use PVE::Network::SDN::Vnets;
use PVE::Network::SDN::Zones;
use PVE::Firewall;
+use PVE::QemuServer::Network;
+
my $iface = shift;
my $hotplug = 0;
@@ -36,7 +37,7 @@ $netconf = $conf->{pending}->{$netid} if !$migratedfrom && defined($conf->{pendi
die "unable to get network config '$netid'\n"
if !defined($netconf);
-my $net = PVE::QemuServer::parse_net($netconf);
+my $net = PVE::QemuServer::Network::parse_net($netconf);
die "unable to parse network config '$netid'\n" if !$net;
# The nftable-based implementation from the newer proxmox-firewall does not requires FW bridges
--
2.47.2
_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel
next prev parent reply other threads:[~2025-06-25 15:59 UTC|newest]
Thread overview: 33+ messages / expand[flat|nested] mbox.gz Atom feed top
2025-06-25 15:56 [pve-devel] [PATCH-SERIES qemu-server 00/31] preparation for blockdev, part three Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 01/31] print ovmf commandline: collect hardware parameters into hash argument Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 02/31] introduce OVMF module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 03/31] ovmf: add support for using blockdev Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 04/31] cfg2cmd: ovmf: support print_ovmf_commandline() returning machine flags Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 05/31] assume that SDN is available Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 06/31] schema: remove unused pve-qm-ipconfig standard option Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 07/31] remove unused $nic_model_list_txt variable Fiona Ebner
2025-06-25 15:56 ` Fiona Ebner [this message]
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 09/31] agent: drop unused $noerr argument from helpers Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 10/31] agent: code style: order module imports according to style guide Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 11/31] agent: avoid dependency on QemuConfig module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 12/31] agent: avoid use of deprecated check_running() function Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 13/31] agent: move qga_check_running() to agent module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 14/31] move find_vmstate_storage() helper to QemuConfig module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 15/31] introduce QemuMigrate::Helpers module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 16/31] introduce RunState module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 17/31] code cleanup: drive mirror: do not prefix calls to function in the same module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 18/31] introduce BlockJob module Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 19/31] drive: die in get_drive_id() if argument misses relevant members Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 20/31] block job: add and use wrapper for mirror Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 21/31] drive mirror: add variable for device ID and make name for drive ID precise Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 22/31] test: migration: factor out common mocking for mirror Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 23/31] block job: factor out helper for common mirror QMP options Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 24/31] block job: add blockdev mirror Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 25/31] blockdev: support using zeroinit filter Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [PATCH qemu-server 26/31] blockdev: make some functions private Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 27/31] clone disk: skip check for aio=default (io_uring) compatibility starting with machine version 10.0 Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 28/31] print drive device: don't reference any drive for 'none' " Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 29/31] blockdev: add support for NBD paths Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 30/31] command line: switch to blockdev starting with machine version 10.0 Fiona Ebner
2025-06-25 15:56 ` [pve-devel] [RFC qemu-server 31/31] test: migration: update running machine to 10.0 Fiona Ebner
2025-06-26 13:09 ` [pve-devel] partially-applied: [PATCH-SERIES qemu-server 00/31] preparation for blockdev, part three Fabian Grünbichler
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20250625155751.268047-9-f.ebner@proxmox.com \
--to=f.ebner@proxmox.com \
--cc=pve-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal