Also gets rid of a cyclic dependency between the main QemuServer module and the Cloudinit module.
Signed-off-by: Fiona Ebner <f.eb...@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