[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