[pve-devel] [PATCH qemu-server 08/31] introduce Network module
Fiona Ebner
f.ebner at proxmox.com
Wed Jun 25 17:56:31 CEST 2025
Also gets rid of a cyclic dependency between the main QemuServer
module and the Cloudinit module.
Signed-off-by: Fiona Ebner <f.ebner at 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
More information about the pve-devel
mailing list