From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [212.224.123.68]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits)) (No client certificate requested) by lists.proxmox.com (Postfix) with ESMTPS id 77ED39A315 for ; Fri, 17 Nov 2023 12:40:29 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 7DA0C30D11 for ; Fri, 17 Nov 2023 12:40:26 +0100 (CET) Received: from lana.proxmox.com (unknown [94.136.29.99]) by firstgate.proxmox.com (Proxmox) with ESMTP for ; Fri, 17 Nov 2023 12:40:19 +0100 (CET) Received: by lana.proxmox.com (Postfix, from userid 10043) id 27DCB2C339D; Fri, 17 Nov 2023 12:40:18 +0100 (CET) From: Stefan Hanreich To: pve-devel@lists.proxmox.com Date: Fri, 17 Nov 2023 12:39:43 +0100 Message-Id: <20231117114011.834002-6-s.hanreich@proxmox.com> X-Mailer: git-send-email 2.39.2 In-Reply-To: <20231117114011.834002-1-s.hanreich@proxmox.com> References: <20231117114011.834002-1-s.hanreich@proxmox.com> MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-SPAM-LEVEL: Spam detection results: 0 AWL -0.453 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment KAM_LAZY_DOMAIN_SECURITY 1 Sending domain does not have any anti-forgery methods RDNS_NONE 0.793 Delivered to internal network by a host with no rDNS SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_NONE 0.001 SPF: sender does not publish an SPF Record T_SCC_BODY_TEXT_LINE -0.01 - Subject: [pve-devel] [PATCH v4 pve-network 05/33] ipam: plugins: preparations for DHCP X-BeenThere: pve-devel@lists.proxmox.com X-Mailman-Version: 2.1.29 Precedence: list List-Id: Proxmox VE development discussion List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , X-List-Received-Date: Fri, 17 Nov 2023 11:40:29 -0000 Adds a new file priv/macs.db for caching the queries to IPAM. Additionally adds and imeplements methods to the IPAM plugins that are required for the DHCP functionality. Co-Authored-By: Alexandre Derumier Signed-off-by: Stefan Hanreich --- src/PVE/Network/SDN/Ipams.pm | 80 +++++++++++++++++++- src/PVE/Network/SDN/Ipams/NetboxPlugin.pm | 86 ++++++++++++++++++++-- src/PVE/Network/SDN/Ipams/PVEPlugin.pm | 85 +++++++++++++++++++-- src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm | 29 ++++++++ src/PVE/Network/SDN/Ipams/Plugin.pm | 19 ++++- 5 files changed, 281 insertions(+), 18 deletions(-) diff --git a/src/PVE/Network/SDN/Ipams.pm b/src/PVE/Network/SDN/Ipams.pm index e8a4b0b..926df90 100644 --- a/src/PVE/Network/SDN/Ipams.pm +++ b/src/PVE/Network/SDN/Ipams.pm @@ -4,9 +4,10 @@ use strict; use warnings; use JSON; +use Net::IP; use PVE::Tools qw(extract_param dir_glob_regex run_command); -use PVE::Cluster qw(cfs_read_file cfs_write_file cfs_lock_file); +use PVE::Cluster qw(cfs_register_file cfs_read_file cfs_write_file cfs_lock_file); use PVE::Network; use PVE::Network::SDN::Ipams::PVEPlugin; @@ -19,6 +20,64 @@ PVE::Network::SDN::Ipams::NetboxPlugin->register(); PVE::Network::SDN::Ipams::PhpIpamPlugin->register(); PVE::Network::SDN::Ipams::Plugin->init(); +my $macdb_filename = 'priv/macs.db'; + +cfs_register_file($macdb_filename, \&json_reader, \&json_writer); + +sub json_reader { + my ($filename, $data) = @_; + + return defined($data) && length($data) > 0 ? decode_json($data) : {}; +} + +sub json_writer { + my ($filename, $data) = @_; + + return encode_json($data); +} + +sub read_macdb { + my () = @_; + + return cfs_read_file($macdb_filename); +} + +sub write_macdb { + my ($data) = @_; + + cfs_write_file($macdb_filename, $data); +} + +sub add_cache_mac_ip { + my ($mac, $ip) = @_; + + cfs_lock_file($macdb_filename, undef, sub { + my $db = read_macdb(); + if (Net::IP::ip_is_ipv4($ip)) { + $db->{macs}->{$mac}->{ip4} = $ip; + } else { + $db->{macs}->{$mac}->{ip6} = $ip; + } + write_macdb($db); + }); + warn "$@" if $@; +} + +sub del_cache_mac_ip { + my ($mac, $ip) = @_; + + cfs_lock_file($macdb_filename, undef, sub { + my $db = read_macdb(); + if (Net::IP::ip_is_ipv4($ip)) { + delete $db->{macs}->{$mac}->{ip4}; + } else { + delete $db->{macs}->{$mac}->{ip6}; + } + delete $db->{macs}->{$mac} if !defined($db->{macs}->{$mac}->{ip4}) && !defined($db->{macs}->{$mac}->{ip6}); + write_macdb($db); + }); + warn "$@" if $@; +} sub sdn_ipams_config { my ($cfg, $id, $noerr) = @_; @@ -39,8 +98,8 @@ sub config { } sub get_plugin_config { - my ($vnet) = @_; - my $ipamid = $vnet->{ipam}; + my ($zone) = @_; + my $ipamid = $zone->{ipam}; my $ipam_cfg = PVE::Network::SDN::Ipams::config(); return $ipam_cfg->{ids}->{$ipamid}; } @@ -65,5 +124,20 @@ sub complete_sdn_vnet { return $cmdname eq 'add' ? [] : [ PVE::Network::SDN::Vnets::sdn_ipams_ids($cfg) ]; } +sub get_ips_from_mac { + my ($mac, $zoneid, $zone) = @_; + + my $macdb = read_macdb(); + return ($macdb->{macs}->{$mac}->{ip4}, $macdb->{macs}->{$mac}->{ip6}) if $macdb->{macs}->{$mac}; + + my $plugin_config = get_plugin_config($zone); + my $plugin = PVE::Network::SDN::Ipams::Plugin->lookup($plugin_config->{type}); + ($macdb->{macs}->{$mac}->{ip4}, $macdb->{macs}->{$mac}->{ip6}) = $plugin->get_ips_from_mac($plugin_config, $mac, $zoneid); + + write_macdb($macdb); + + return ($macdb->{macs}->{$mac}->{ip4}, $macdb->{macs}->{$mac}->{ip6}); +} + 1; diff --git a/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm b/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm index f0e7168..91010bb 100644 --- a/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm +++ b/src/PVE/Network/SDN/Ipams/NetboxPlugin.pm @@ -77,14 +77,20 @@ sub del_subnet { } sub add_ip { - my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $noerr) = @_; my $mask = $subnet->{mask}; my $url = $plugin_config->{url}; my $token = $plugin_config->{token}; my $section = $plugin_config->{section}; my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; - $description .= " mac:$mac" if $mac && $description; + + my $description = undef; + if ($is_gateway) { + $description = 'gateway' + } elsif ($mac) { + $description = "mac:$mac"; + } my $params = { address => "$ip/$mask", dns_name => $hostname, description => $description }; @@ -102,14 +108,20 @@ sub add_ip { } sub update_ip { - my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $noerr) = @_; my $mask = $subnet->{mask}; my $url = $plugin_config->{url}; my $token = $plugin_config->{token}; my $section = $plugin_config->{section}; my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; - $description .= " mac:$mac" if $mac && $description; + + my $description = undef; + if ($is_gateway) { + $description = 'gateway' + } elsif ($mac) { + $description = "mac:$mac"; + } my $params = { address => "$ip/$mask", dns_name => $hostname, description => $description }; @@ -125,7 +137,7 @@ sub update_ip { } sub add_next_freeip { - my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description, $noerr) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $vmid, $noerr) = @_; my $cidr = $subnet->{cidr}; @@ -134,7 +146,8 @@ sub add_next_freeip { my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; my $internalid = get_prefix_id($url, $cidr, $headers); - $description .= " mac:$mac" if $mac && $description; + + my $description = "mac:$mac" if $mac; my $params = { dns_name => $hostname, description => $description }; @@ -151,6 +164,33 @@ sub add_next_freeip { return $ip; } +sub add_range_next_freeip { + my ($class, $plugin_config, $subnet, $range, $data, $noerr) = @_; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $internalid = get_iprange_id($url, $range, $headers); + my $description = "mac:$data->{mac}" if $data->{mac}; + + my $params = { dns_name => $data->{hostname}, description => $description }; + + my $ip = undef; + eval { + my $result = PVE::Network::SDN::api_request("POST", "$url/ipam/ip-ranges/$internalid/available-ips/", $headers, $params); + $ip = $result->{address}; + print "found ip free $ip in range $range->{'start-address'}-$range->{'end-address'}\n" if $ip; + }; + + if ($@) { + die "can't find free ip in range $range->{'start-address'}-$range->{'end-address'}: $@" if !$noerr; + } + + return $ip; + +} + sub del_ip { my ($class, $plugin_config, $subnetid, $subnet, $ip, $noerr) = @_; @@ -171,6 +211,31 @@ sub del_ip { } } +sub get_ips_from_mac { + my ($class, $plugin_config, $mac, $zoneid) = @_; + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Authorization' => "token $token"]; + + my $ip4 = undef; + my $ip6 = undef; + + my $data = PVE::Network::SDN::api_request("GET", "$url/ipam/ip-addresses/?description__ic=$mac", $headers); + for my $ip (@{$data->{results}}) { + if ($ip->{family}->{value} == 4 && !$ip4) { + ($ip4, undef) = split(/\//, $ip->{address}); + } + + if ($ip->{family}->{value} == 6 && !$ip6) { + ($ip6, undef) = split(/\//, $ip->{address}); + } + } + + return ($ip4, $ip6); +} + + sub verify_api { my ($class, $plugin_config) = @_; @@ -204,6 +269,15 @@ sub get_prefix_id { return $internalid; } +sub get_iprange_id { + my ($url, $range, $headers) = @_; + + my $result = PVE::Network::SDN::api_request("GET", "$url/ipam/ip-ranges/?start_address=$range->{'start-address'}&end_address=$range->{'end-address'}", $headers); + my $data = @{$result->{results}}[0]; + my $internalid = $data->{id}; + return $internalid; +} + sub get_ip_id { my ($url, $ip, $headers) = @_; my $result = PVE::Network::SDN::api_request("GET", "$url/ipam/ip-addresses/?q=$ip", $headers); diff --git a/src/PVE/Network/SDN/Ipams/PVEPlugin.pm b/src/PVE/Network/SDN/Ipams/PVEPlugin.pm index 3e8ffc5..a5b4fe7 100644 --- a/src/PVE/Network/SDN/Ipams/PVEPlugin.pm +++ b/src/PVE/Network/SDN/Ipams/PVEPlugin.pm @@ -82,7 +82,7 @@ sub del_subnet { } sub add_ip { - my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway) = @_; my $cidr = $subnet->{cidr}; my $zone = $subnet->{zone}; @@ -96,8 +96,17 @@ sub add_ip { die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet; die "IP '$ip' already exist\n" if (!$is_gateway && defined($dbsubnet->{ips}->{$ip})) || ($is_gateway && defined($dbsubnet->{ips}->{$ip}) && !defined($dbsubnet->{ips}->{$ip}->{gateway})); - $dbsubnet->{ips}->{$ip} = {}; - $dbsubnet->{ips}->{$ip} = {gateway => 1} if $is_gateway; + + my $data = {}; + if ($is_gateway) { + $data->{gateway} = 1; + } else { + $data->{vmid} = $vmid if $vmid; + $data->{hostname} = $hostname if $hostname; + $data->{mac} = $mac if $mac; + } + + $dbsubnet->{ips}->{$ip} = $data; write_db($db); }); @@ -105,12 +114,12 @@ sub add_ip { } sub update_ip { - my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway) = @_; return; } sub add_next_freeip { - my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $vmid, $noerr) = @_; my $cidr = $subnet->{cidr}; my $network = $subnet->{network}; @@ -156,6 +165,39 @@ sub add_next_freeip { return "$freeip/$mask"; } +sub add_range_next_freeip { + my ($class, $plugin_config, $subnet, $range, $data, $noerr) = @_; + + my $cidr = $subnet->{cidr}; + my $zone = $subnet->{zone}; + + cfs_lock_file($ipamdb_file, undef, sub { + my $db = read_db(); + + my $dbzone = $db->{zones}->{$zone}; + die "zone '$zone' doesn't exist in IPAM DB\n" if !$dbzone; + + my $dbsubnet = $dbzone->{subnets}->{$cidr}; + die "subnet '$cidr' doesn't exist in IPAM DB\n" if !$dbsubnet; + + my $ip = new Net::IP ("$range->{'start-address'} - $range->{'end-address'}") + or die "Invalid IP address(es) in Range!\n"; + my $mac = $data->{mac}; + + do { + my $ip_address = $ip->version() == 6 ? $ip->short() : $ip->ip(); + if (!$dbsubnet->{ips}->{$ip_address}) { + $dbsubnet->{ips}->{$ip_address} = $data; + write_db($db); + + return $ip_address; + } + } while (++$ip); + + die "No free IP left in Range $range->{'start-address'}:$range->{'end-address'}}\n"; + }); +} + sub del_ip { my ($class, $plugin_config, $subnetid, $subnet, $ip) = @_; @@ -172,12 +214,43 @@ sub del_ip { die "IP '$ip' does not exist in IPAM DB\n" if !defined($dbsubnet->{ips}->{$ip}); delete $dbsubnet->{ips}->{$ip}; - write_db($db); }); die "$@" if $@; } +sub get_ips_from_mac { + my ($class, $plugin_config, $mac, $zoneid) = @_; + + #just in case, as this should already be cached in local macs.db + + my $ip4 = undef; + my $ip6 = undef; + + my $db = read_db(); + die "zone $zoneid don't exist in ipam db" if !$db->{zones}->{$zoneid}; + my $dbzone = $db->{zones}->{$zoneid}; + my $subnets = $dbzone->{subnets}; + + for my $subnet ( keys %$subnets) { + next if Net::IP::ip_is_ipv4($subnet) && $ip4; + next if $ip6; + my $ips = $subnets->{$subnet}->{ips}; + for my $ip (keys %$ips) { + my $ipobject = $ips->{$ip}; + if ($ipobject->{mac} && $ipobject->{mac} eq $mac) { + if (Net::IP::ip_is_ipv4($ip)) { + $ip4 = $ip; + } else { + $ip6 = $ip; + } + } + } + last if $ip4 && $ip6; + } + return ($ip4, $ip6); +} + #helpers sub read_db { diff --git a/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm b/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm index ad5286b..1b7b666 100644 --- a/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm +++ b/src/PVE/Network/SDN/Ipams/PhpIpamPlugin.pm @@ -204,6 +204,35 @@ sub del_ip { } } +sub get_ips_from_mac { + my ($class, $plugin_config, $mac, $zoneid) = @_; + + + my $url = $plugin_config->{url}; + my $token = $plugin_config->{token}; + my $headers = ['Content-Type' => 'application/json; charset=UTF-8', 'Token' => $token]; + + my $ip4 = undef; + my $ip6 = undef; + + my $ips = PVE::Network::SDN::api_request("GET", "$url/addresses/search_mac/$mac", $headers); + + #fixme + die "parsing of result not yet implemented"; + + for my $ip (@$ips) { +# if ($ip->{family}->{value} == 4 && !$ip4) { +# ($ip4, undef) = split(/\//, $ip->{address}); +# } +# +# if ($ip->{family}->{value} == 6 && !$ip6) { +# ($ip6, undef) = split(/\//, $ip->{address}); +# } + } + + return ($ip4, $ip6); +} + sub verify_api { my ($class, $plugin_config) = @_; diff --git a/src/PVE/Network/SDN/Ipams/Plugin.pm b/src/PVE/Network/SDN/Ipams/Plugin.pm index c96eeda..05d1416 100644 --- a/src/PVE/Network/SDN/Ipams/Plugin.pm +++ b/src/PVE/Network/SDN/Ipams/Plugin.pm @@ -79,13 +79,13 @@ sub del_subnet { } sub add_ip { - my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $noerr) = @_; die "please implement inside plugin"; } sub update_ip { - my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $description, $is_gateway, $noerr) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $ip, $hostname, $mac, $vmid, $is_gateway, $noerr) = @_; # only update ip attributes (mac,hostname,..). Don't change the ip addresses itself, as some ipam # don't allow ip address change without del/add @@ -93,7 +93,14 @@ sub update_ip { } sub add_next_freeip { - my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $description, $noerr) = @_; + my ($class, $plugin_config, $subnetid, $subnet, $hostname, $mac, $vmid, $noerr) = @_; + + die "please implement inside plugin"; +} + + +sub add_range_next_freeip { + my ($class, $plugin_config, $subnet, $range, $data, $noerr) = @_; die "please implement inside plugin"; } @@ -104,6 +111,12 @@ sub del_ip { die "please implement inside plugin"; } +sub get_ips_from_mac { + my ($class, $plugin_config, $mac, $zone) = @_; + + die "please implement inside plugin"; +} + sub on_update_hook { my ($class, $plugin_config) = @_; } -- 2.39.2