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 A5FC2B9F7C for ; Mon, 18 Mar 2024 13:41:33 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8D36411031 for ; Mon, 18 Mar 2024 13:41:33 +0100 (CET) Received: from proxmox-new.maurer-it.com (proxmox-new.maurer-it.com [94.136.29.106]) (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 firstgate.proxmox.com (Proxmox) with ESMTPS for ; Mon, 18 Mar 2024 13:41:31 +0100 (CET) Received: from proxmox-new.maurer-it.com (localhost.localdomain [127.0.0.1]) by proxmox-new.maurer-it.com (Proxmox) with ESMTP id 4E6594660E for ; Mon, 18 Mar 2024 13:41:31 +0100 (CET) Content-Type: text/plain; charset=UTF-8 Date: Mon, 18 Mar 2024 13:41:28 +0100 Message-Id: From: "Max Carrara" To: "Proxmox VE development discussion" Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Mailer: aerc 0.17.0-72-g6a84f1331f1c References: <20240103153753.407079-3-s.lendl@proxmox.com> <20240103153753.407079-11-s.lendl@proxmox.com> In-Reply-To: <20240103153753.407079-11-s.lendl@proxmox.com> X-SPAM-LEVEL: Spam detection results: 0 AWL 0.022 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 SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record T_SCC_BODY_TEXT_LINE -0.01 - Subject: Re: [pve-devel] [PATCH pve-network 8/8] test(vnets): add test_vnets_blackbox 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: Mon, 18 Mar 2024 12:41:33 -0000 On Wed Jan 3, 2024 at 4:37 PM CET, Stefan Lendl wrote: > Add several tests for Vnets. State setup as well as testing results is > done only via the API to test on the API boundaries not not against the > internal state. Internal state is mocked to avoid requiring access to > system files or pmxcfs. > > Mocking is done by reading and writing to a hash that holds the entire > state of SDN. The state is reset after every test run. > > Testing is done via helper functions: nic_join and nic_start. > When a nic joins a Vnet, currently it always - and only - calls > add_next_free_cidr(). The same is true if a nic starts on Vnet, which > only calles add_dhcp_mapping. > > These test functions homogenize the parameter list in contrast to the > current calls to the current functions. These functions should move to > Vnets.pm to be called from QemuServer and LXC! > > The run_test function takes a function pointer and passes the rest of > the arguments to the test functions after resetting the test state. > This allows fine-grained parameterization per-test directly in the code > instead of separated files that require the entire state to be passed > in. > > The tests setup the SDN by creating a simple zone and a simple vnet. The > nic_join and nic_start function is called with different subnet > configuration wiht and without a dhcp-range configured and with or > without an already present IP in the IPAM. I really like where this is going! Now that I've read through this patch, it's become clear why you factored so many calls to commands etc. into their own `sub`s. Since you mentioned that this is more of an RFC off-list, I get why there are a bunch of lines that are commented out at the moment. Those obviously shouldn't be committed later on. > > Several of the tests fail and uncovers bugs, that shall be fixed in > subsequent commits. Would be nice to perhaps also have those in the final series though ;) Another thing that stood out to me is that some cases could be declarative, e.g. the cases for `test_nic_join` and `test_nic_start` could be declared in an array for each. You could then just loop over the cases - that makes it easier to `plan` those cases later on. That being said, you could perhaps structure the whole script so that you call a `sub` named e.g. `setup` where you - well - set up all the required logic and perform checks for the necessary pre-conditions, then another `sub` that runs the tests (and optionally one that cleans things up if necessary). Though, please note that this is not a strict necessity from my side, just something I wanted to mention! I like the way you've written your tests a lot, it's just that I personally tend to prefer a more declarative approach. So, it's okay if you just leave the structure as it is right now, if you prefer it that way. There are some more comments inline that give a little more context regarding the above, but otherwise, LGTM - pretty good to see more testing to be done! > > Signed-off-by: Stefan Lendl > --- > src/test/Makefile | 5 +- > src/test/run_test_vnets_blackbox.pl | 797 ++++++++++++++++++++++++++++ > 2 files changed, 801 insertions(+), 1 deletion(-) > create mode 100755 src/test/run_test_vnets_blackbox.pl > > diff --git a/src/test/Makefile b/src/test/Makefile > index eb59d5f..5a937a4 100644 > --- a/src/test/Makefile > +++ b/src/test/Makefile > @@ -1,6 +1,6 @@ > all: test > =20 > -test: test_zones test_ipams test_dns test_subnets > +test: test_zones test_ipams test_dns test_subnets test_vnets_blackbox > =20 > test_zones: run_test_zones.pl > ./run_test_zones.pl > @@ -14,4 +14,7 @@ test_dns: run_test_dns.pl > test_subnets: run_test_subnets.pl > ./run_test_subnets.pl > =20 > +test_vnets_blackbox: run_test_vnets_blackbox.pl > + ./run_test_vnets_blackbox.pl > + > clean: > diff --git a/src/test/run_test_vnets_blackbox.pl b/src/test/run_test_vnet= s_blackbox.pl > new file mode 100755 > index 0000000..2fd5bd7 > --- /dev/null > +++ b/src/test/run_test_vnets_blackbox.pl > @@ -0,0 +1,797 @@ > +#!/usr/bin/perl > + > +use strict; > +use warnings; > + > +use lib qw(..); > +use File::Slurp; > +use List::Util qw(first all); > +use NetAddr::IP qw(:lower); > + > +use Test::More; > +use Test::MockModule; > + > +use PVE::Tools qw(extract_param file_set_contents); > + > +use PVE::Network::SDN; > +use PVE::Network::SDN::Zones; > +use PVE::Network::SDN::Zones::Plugin; > +use PVE::Network::SDN::Controllers; > +use PVE::Network::SDN::Dns; > +use PVE::Network::SDN::Vnets; > + > +use PVE::RESTEnvironment; > + > +use PVE::API2::Network::SDN::Zones; > +use PVE::API2::Network::SDN::Subnets; > +use PVE::API2::Network::SDN::Vnets; > +use PVE::API2::Network::SDN::Ipams; > + > +use Data::Dumper qw(Dumper); > +$Data::Dumper::Sortkeys =3D 1; > +$Data::Dumper::Indent =3D 1; > + > +my $TMP_ETHERS_FILE =3D "/tmp/ethers"; > + > +my $test_state =3D undef; > +sub clear_test_state { > + $test_state =3D { > + locks =3D> {}, > + datacenter_config =3D> {}, > + subnets_config =3D> {}, > + controller_config =3D> {}, > + dns_config =3D> {}, > + zones_config =3D> {}, > + vnets_config =3D> {}, > + macdb =3D> {}, > + ipamdb =3D> {}, > + ipam_config =3D> { > + 'ids' =3D> { > + 'pve' =3D> { > + 'type' =3D> 'pve' > + }, > + } > + }, > + }; > + PVE::Tools::file_set_contents($TMP_ETHERS_FILE, "\n"); > +} > +clear_test_state(); > + > +my $mocked_cfs_lock_file =3D sub { > + my ($filename, $timeout, $code, @param) =3D @_; > + > + die "$filename already locked\n" if ($test_state->{locks}->{$filenam= e}); > + > + $test_state->{locks}->{$filename} =3D 1; > + > + my $res =3D eval { $code->(@param); }; > + > + delete $test_state->{locks}->{$filename}; > + > + return $res; > +}; > + > +sub read_sdn_config { > + my ($file) =3D @_; > + # Read structure back in again > + open my $in, '<', $file or die $!; > + my $sdn_config; > + { > + local $/; # slurp mode > + $sdn_config =3D eval <$in>; I was about to say that perlcritic complains about the above line, but then noticed that we're using the same sub in other tests as well. However, it's not used in this file at all, so it should probably be removed for now (and be added when it's needed). > + } > + close $in; > + return $sdn_config; > +} > + > +my $mocked_pve_sdn; > +$mocked_pve_sdn =3D Test::MockModule->new('PVE::Network::SDN'); > +$mocked_pve_sdn->mock( > + cfs_lock_file =3D> $mocked_cfs_lock_file, > +); > + > +my $mocked_pve_tools =3D Test::MockModule->new('PVE::Tools'); > +$mocked_pve_tools->mock( > + lock_file =3D> $mocked_cfs_lock_file, > +); > + > + > +# my $test_state->{zones_config} =3D {}; > +my $mocked_sdn_zones; > +$mocked_sdn_zones =3D Test::MockModule->new('PVE::Network::SDN::Zones'); > +$mocked_sdn_zones->mock( > + config =3D> sub { > + # print "mocked zones\n"; > + # warn Dumper($test_state).' '; > + # print Dumper($test_state->{zones_config}); > + return $test_state->{zones_config}; > + }, > + write_config =3D> sub { > + my ($cfg) =3D @_; > + # print "mocked write_config zones\n"; > + # print Dumper($cfg); > + $test_state->{zones_config} =3D $cfg; > + }, > +); > + > +# my $datacenter_config =3D undef; > +my $mocked_sdn_zones_super_plugin; > +$mocked_sdn_zones_super_plugin =3D Test::MockModule->new('PVE::Network::= SDN::Zones::Plugin'); > +$mocked_sdn_zones_super_plugin->mock( > + datacenter_config =3D> sub { > + # print "mocked datacenter_config\n"; > + return $test_state->{datacenter_config}; > + }, > +); > + > + > +# my $test_state->{vnets_config} =3D {}; > +my $mocked_sdn_vnets; > +$mocked_sdn_vnets =3D Test::MockModule->new('PVE::Network::SDN::Vnets'); > +$mocked_sdn_vnets->mock( > + config =3D> sub { > + # print "mocked vnets\n"; > + # warn Dumper($test_state->{vnets_config}).' '; > + return $test_state->{vnets_config}; > + }, > + write_config =3D> sub { > + my ($cfg) =3D @_; > + # print "mocked vnets write_config\n"; > + # warn Dumper($cfg, $test_state->{vnets_config}).' '; > + $test_state->{vnets_config} =3D $cfg; > + }, > + cfs_lock_file =3D> $mocked_cfs_lock_file, > +); > + > + > +# my $test_state->{subnets_config} =3D undef; > +my $mocked_sdn_subnets; > +$mocked_sdn_subnets =3D Test::MockModule->new('PVE::Network::SDN::Subnet= s'); > +$mocked_sdn_subnets->mock( > + config =3D> sub { > + # print "mocked subnet\n"; > + return $test_state->{subnets_config}; > + }, > + write_config =3D> sub { > + my ($cfg) =3D @_; > + $test_state->{subnets_config} =3D $cfg; > + }, > + cfs_lock_file =3D> $mocked_cfs_lock_file, > + # verify_dns_zone =3D> sub { > + # return; > + # }, > + # add_dns_record =3D> sub { > + # return; > + # } > +); > + > +# my $test_state->{controller_config} =3D undef; > +my $mocked_sdn_controller; > +$mocked_sdn_controller =3D Test::MockModule->new('PVE::Network::SDN::Con= trollers'); > +$mocked_sdn_controller->mock( > + config =3D> sub { > + # print "mocked controller\n"; > + return $test_state->{controller_config}; > + }, > + write_config =3D> sub { > + my ($cfg) =3D @_; > + $test_state->{controller_config} =3D $cfg; > + }, > + cfs_lock_file =3D> $mocked_cfs_lock_file, > +); > + > + > +my $mocked_sdn_dns; > +$mocked_sdn_dns =3D Test::MockModule->new('PVE::Network::SDN::Dns'); > +$mocked_sdn_dns->mock( > + config =3D> sub { > + # print "mocked dns\n"; > + return $test_state->{dns_config}; > + }, > + write_config =3D> sub { > + my ($cfg) =3D @_; > + $test_state->{dns_config} =3D $cfg; > + }, > + cfs_lock_file =3D> $mocked_cfs_lock_file, > +); > + > + > +my $mocked_sdn_ipams; > +$mocked_sdn_ipams =3D Test::MockModule->new('PVE::Network::SDN::Ipams'); > +$mocked_sdn_ipams->mock( > + config =3D> sub { > + # print "mocked ipam config\n"; > + return $test_state->{ipam_config}; > + }, > + write_config =3D> sub { > + my ($cfg) =3D @_; > + $test_state->{ipam_config} =3D $cfg; > + }, > + read_macdb =3D> sub { > + return $test_state->{macdb}; > + }, > + write_macdb =3D> sub { > + my ($cfg) =3D @_; > + # print "write mocked macsdb\n"; > + $test_state->{macdb} =3D $cfg; > + }, > + cfs_lock_file =3D> $mocked_cfs_lock_file, > +); > + > +# my $test_state->{ipamdb} =3D {}; > +my $ipam_plugin =3D PVE::Network::SDN::Ipams::Plugin->lookup("pve"); # F= IXME hardcoded to pve > +my $mocked_ipam_plugin =3D Test::MockModule->new($ipam_plugin); > +$mocked_ipam_plugin->mock( > + read_db =3D> sub { > + # print "mocked ipamdb\n"; > + return $test_state->{ipamdb}; > + }, > + write_db =3D> sub { > + my ($cfg) =3D @_; > + # print "mocked write ipamdb\n"; > + $test_state->{ipamdb} =3D $cfg; > + }, > + cfs_lock_file =3D> $mocked_cfs_lock_file, > +); > + > +my $mocked_sdn_dhcp_dnsmasq =3D Test::MockModule->new('PVE::Network::SDN= ::Dhcp::Dnsmasq'); > +$mocked_sdn_dhcp_dnsmasq->mock( > + ethers_file =3D> sub { > + return "/tmp/ethers"; > + }, > + update_lease =3D> sub { > + my ($dhcpid, $ip4, $mac) =3D @_; > + }, > + assert_dnsmasq_installed =3D> sub { > + return 1; > + }, > + before_configure =3D> sub { > + my ($class, $dhcpid) =3D @_; > + }, > + systemctl_service =3D> sub { > + my ($action, $service) =3D @_; > + # print "mocked `systemctl $action $service`\n"; > + }, > +); > + > +my $mocked_api_zones =3D Test::MockModule->new('PVE::API2::Network::SDN:= :Zones'); > +$mocked_api_zones->mock( > + 'create_etc_interfaces_sdn_dir', sub {}, > +); > + > +my $rpcenv =3D PVE::RESTEnvironment->init('priv'); > +$rpcenv->init_request(); > +$rpcenv->set_language("en_US.UTF-8"); > +$rpcenv->set_user('root@pam'); Regarding my comment about structure further above, setting up the rpcenv here is something that I personally would prefer to have in a `sub setup { ... }` or similar, just so it's easier to track what's being set up and where. > + > +my $mocked_rpc_env_obj =3D Test::MockModule->new('PVE::RESTEnvironment')= ; > +$mocked_rpc_env_obj->mock( > + check_any =3D> sub { > + my ($self, $user, $path, $privs, $noerr) =3D @_; > + # print "mocked check_any\n"; > + return 1; > + }, > +); > + > +# ------- TEST FUNCTIONS -------------- > + > +sub nic_join { > + my ($vnetid, $mac, $hostname, $vmid) =3D @_; > + return PVE::Network::SDN::Vnets::add_next_free_cidr($vnetid, $hostna= me, $mac, "$vmid", undef, 1); > +} > + > +sub nic_leave { > + my ($vnetid, $mac, $hostname) =3D @_; > + return PVE::Network::SDN::Vnets::del_ips_from_mac($vnetid, $mac, $ho= stname); > +} > + > +sub nic_start { > + my ($vnetid, $mac, $vmid, $hostname) =3D @_; > + return PVE::Network::SDN::Vnets::add_dhcp_mapping($vnetid, $mac, $vm= id, $hostname); > +} > + > + > +# ---- API HELPER FUNCTIONS FOR THE TESTS ----- > + > +my $t_invalid; > +sub get_zone { > + my ($id) =3D @_; > + return eval { PVE::API2::Network::SDN::Zones->read({zone =3D> $id});= }; > +} > +# verify get_zone actually fails if invalid > +# FIXME re-add > +# my $t_invalid =3D get_zone("invalid"); > +# die("getting an invalid zone must fail") if (!$@); > +# fail("getting an invalid zone must fail") if (defined $t_invalid); > + > +sub create_zone { > + my ($params) =3D @_; > + my $zoneid =3D $params->{zone}; > + # die if failed! > + eval { PVE::API2::Network::SDN::Zones->create($params); }; > + die("creating zone failed: $@") if ($@); > + > + my $zone =3D get_zone($zoneid); > + die ("test setup: zone ($zoneid) not defined") if (!defined $zone); > + return $zone; > +} > + > +sub get_vnet { > + my ($id) =3D @_; > + return eval { PVE::API2::Network::SDN::Vnets->read({vnet =3D> $id});= }; > +} > +# verify get_vnet > +$t_invalid =3D get_vnet("invalid"); > +die("getting an invalid vnet must fail") if (!$@); > +fail("getting an invalid vnet must fail") if (defined $t_invalid); > + > +sub create_vnet { > + my ($params) =3D @_; > + my $vnetid =3D $params->{vnet}; > + PVE::API2::Network::SDN::Vnets->create($params); > + > + my $vnet =3D get_vnet($vnetid); > + die ("test setup: vnet ($vnetid) not defined") if (!defined $vnet); > + return $vnet; > +} > + > +sub get_subnet { > + my ($id) =3D @_; > + return eval { PVE::API2::Network::SDN::Subnets->read({subnet =3D> $i= d}); }; > +} > +# verify get_subnet > +$t_invalid =3D get_subnet("invalid"); > +die("getting an invalid subnet must fail") if (!$@); > +fail("getting an invalid subnet must fail") if (defined $t_invalid); > + > +sub create_subnet { > + my ($params) =3D @_; > + PVE::API2::Network::SDN::Subnets->create($params); > + > + # warn Dumper($params).' '; > + # FIXME get_subnet > HOW DO I GET THE ID? > +} > + > +sub get_ipam_entries { > + return PVE::API2::Network::SDN::Ipams->ipamindex({ipam =3D> "pve"}); > +} > + > +sub create_ip { > + my ($param) =3D @_; > + return PVE::API2::Network::SDN::Ips->ipcreate($param); > +} > + > +sub run_test { > + my $test =3D shift; > + clear_test_state(); > + $test->(@_); > +} > + > +sub get_ips_from_mac { > + my ($mac) =3D @_; > + my $ipam_entries =3D get_ipam_entries(); > + return grep { $_->{mac} eq $mac if defined $_->{mac} } $ipam_entries= ->@* if $ipam_entries; > +} > + > +sub get_ip4 { > + my $ip4 =3D first { Net::IP::ip_is_ipv4($_->{ip}) } @_; > + return $ip4->{ip} if defined $ip4; > +} > + > +sub get_ip6 { > + my $ip6 =3D first { Net::IP::ip_is_ipv6($_->{ip}) } @_; > + return $ip6->{ip} if defined $ip6; > +} > + > + > +# -------------- ACTUAL TESTS ----------------------- > + > +sub test_create_vnet_with_gateway { > + my $test_name =3D (split(/::/,(caller(0))[3]))[-1]; > + my $zoneid =3D "TESTZONE"; > + my $vnetid =3D "testvnet"; > + > + my $zone =3D create_zone({ > + type =3D> "simple", > + dhcp =3D> "dnsmasq", > + ipam =3D> "pve", > + zone =3D> $zoneid, > + }); > + > + my $vnet =3D create_vnet({ > + type =3D> "vnet", > + zone =3D> $zoneid, > + vnet =3D> $vnetid, > + }); > + > + create_subnet({ > + type =3D> "subnet", > + vnet =3D> $vnetid, > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }); > + > + my ($p) =3D first { $_->{gateway} =3D=3D 1 } get_ipam_entries()->@*; > + ok ($p, "$test_name: Gateway IP was created in IPAM"); > +} > +run_test(\&test_create_vnet_with_gateway); > + > + > +sub test_without_subnet { > + my $test_name =3D (split(/::/,(caller(0))[3]))[-1]; > + > + my $zoneid =3D "TESTZONE"; > + my $vnetid =3D "testvnet"; > + > + my $zone =3D create_zone({ > + type =3D> "simple", > + dhcp =3D> "dnsmasq", > + ipam =3D> "pve", > + zone =3D> $zoneid, > + }); > + > + my $vnet =3D create_vnet({ > + type =3D> "vnet", > + zone =3D> $zoneid, > + vnet =3D> $vnetid, > + }); > + > + my $hostname =3D "testhostname"; > + my $mac =3D "da:65:8f:18:9b:6f"; > + my $vmid =3D "999"; > + > + eval { > + nic_join($vnetid, $mac, $hostname, $vmid); > + }; > + > + if ($@) { > + fail("$test_name: $@"); > + return; > + } > + > + my @ips =3D grep { $_->{mac} eq $mac } get_ipam_entries()->@*; > + my $num_ips =3D scalar @ips; > + is ($num_ips, 0, "$test_name: No IP allocated in IPAM"); > +} > +run_test(\&test_without_subnet); > + > + > +sub test_nic_join { > + my ($test_name, $subnets) =3D @_; > + > + die "$test_name: we're expecting an array of subnets" if !$subnets; > + my $num_subnets =3D scalar $subnets->@*; > + die "$test_name: we're expecting an array of subnets. $num_subnets e= lements found" if ($num_subnets < 1); > + my $num_dhcp_ranges =3D scalar grep { $_->{'dhcp-range'} } $subnets-= >@*; > + > + my $zoneid =3D "TESTZONE"; > + my $vnetid =3D "testvnet"; > + > + my $zone =3D create_zone({ > + type =3D> "simple", > + dhcp =3D> "dnsmasq", > + ipam =3D> "pve", > + zone =3D> $zoneid, > + }); > + > + my $vnet =3D create_vnet({ > + type =3D> "vnet", > + zone =3D> $zoneid, > + vnet =3D> $vnetid, > + }); > + > + foreach my $subnet ($subnets->@*) { > + $subnet->{type} =3D "subnet"; > + $subnet->{vnet} =3D $vnetid; > + create_subnet($subnet); > + }; > + > + my $hostname =3D "testhostname"; > + my $mac =3D "da:65:8f:18:9b:6f"; > + my $vmid =3D "999"; > + > + eval { > + nic_join($vnetid, $mac, $hostname, $vmid); > + }; > + > + if ($@) { > + fail("$test_name: $@"); > + return; > + } > + > + my @ips =3D get_ips_from_mac($mac); > + my $num_ips =3D scalar @ips; > + is ($num_ips, $num_dhcp_ranges, "$test_name: Expecting $num_dhcp_ran= ges IPs, found $num_ips"); > + ok ((all { ($_->{vnet} eq $vnetid && $_->{zone} eq $zoneid) } @ips), > + "$test_name: all IPs in correct vnet and zone" > + ); > +} > + > +run_test( > + \&test_nic_join, > + "nic_join IPv4 with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, > +]); These kinds of tests are what I meant above - the case above and the ones following below could be put into an array of hashes like so: my $nic_join_cases =3D [ { desc =3D 'nic_join IPv4 with DHCP', subnets =3D [=20 { subnet =3D> '10.0.0.0/24', gateway =3D> '10.0.0.1', 'dhcp-range' =3D> [ 'start-address=3D10.0.0.100,end-address=3D10.0.0.200', ]=20 }, ], }, // ... ]; Just to give an example. > + > +run_test( > + \&test_nic_join, > + "nic_join IPv4 no dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + }, > +]); > + > +run_test( > + \&test_nic_join, > + "nic_join IPv6 with dhcp", > + [{ > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }, > +]); > + > +run_test( > + \&test_nic_join, > + "nic_join IPv6 no dhcp", > + [{ > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + }, > +]); > + > +run_test( > + \&test_nic_join, > + "nic_join IPv4+6 with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }, > +]); > + > +run_test( > + \&test_nic_join, > + "nic_join IPv4+6 no dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + }, > +]); > + > +run_test( > + \&test_nic_join, > + "nic_join IPv4 no DHCP, IPv6 with DHCP", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }, > +]); > + > +run_test( > + \&test_nic_join, > + "nic_join IPv4 with DHCP, IPv6 no DHCP", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + }, > +]); > + > + > +# -------------- nic_start > +sub test_nic_start { > + my ($test_name, $subnets, $current_ip4, $current_ip6) =3D @_; > + > + die "$test_name: we're expecting an array of subnets" if !$subnets; > + my $num_subnets =3D scalar $subnets->@*; > + die "$test_name: we're expecting an array of subnets. $num_subnets e= lements found" if ($num_subnets < 1); > + > + # my $num_expected_ips =3D 0; > + # $num_expected_ips++ if $current_ip4; > + # $num_expected_ips++ if $current_ip6; > + # print "expecting ips: $num_expected_ips\n"; > + > + my $num_expected_ips =3D scalar grep { $_->{'dhcp-range'} } $subnets= ->@*; > + if (defined $current_ip4 && defined $current_ip6) { > + # if we already have an IP for a subnet without a dhcp-range (sp= ecial case for a single test) > + $num_expected_ips =3D 2; > + } > + > + my $zoneid =3D "TESTZONE"; > + my $vnetid =3D "testvnet"; > + > + my $zone =3D create_zone({ > + type =3D> "simple", > + dhcp =3D> "dnsmasq", > + ipam =3D> "pve", > + zone =3D> $zoneid, > + }); > + > + my $vnet =3D create_vnet({ > + type =3D> "vnet", > + zone =3D> $zoneid, > + vnet =3D> $vnetid, > + }); > + > + foreach my $subnet ($subnets->@*) { > + $subnet->{type} =3D "subnet"; > + $subnet->{vnet} =3D $vnetid; > + create_subnet($subnet); > + }; > + > + my $hostname =3D "testhostname"; > + my $mac =3D "da:65:8f:18:9b:6f"; > + my $vmid =3D "999"; > + > + if ($current_ip4) { > + create_ip({ > + zone =3D> $zoneid, > + vnet =3D> $vnetid, > + mac =3D> $mac, > + ip =3D> $current_ip4, > + }); > + } > + > + if ($current_ip6) { > + create_ip({ > + zone =3D> $zoneid, > + vnet =3D> $vnetid, > + mac =3D> $mac, > + ip =3D> $current_ip6, > + }); > + } > + my @current_ips =3D get_ips_from_mac($mac); > + is ( get_ip4(@current_ips), $current_ip4, "$test_name: setup current= IPv4: $current_ip4" ) if defined $current_ip4; > + is ( get_ip6(@current_ips), $current_ip6, "$test_name: setup current= IPv6: $current_ip6" ) if defined $current_ip6; > + > + eval { > + nic_start($vnetid, $mac, $hostname, $vmid); > + }; > + > + if ($@) { > + fail("$test_name: $@"); > + return; > + } > + > + my @ips =3D get_ips_from_mac($mac); > + my $num_ips =3D scalar @ips; > + is ($num_ips, $num_expected_ips, "$test_name: Expecting $num_expecte= d_ips IPs, found $num_ips"); > + ok ((all { ($_->{vnet} eq $vnetid && $_->{zone} eq $zoneid) } @ips), > + "$test_name: all IPs in correct vnet and zone" > + ); > + > + is ( get_ip4(@ips), $current_ip4, "$test_name: still current IPv4: $= current_ip4" ) if $current_ip4; > + is ( get_ip6(@ips), $current_ip6, "$test_name: still current IPv6: $= current_ip6" ) if $current_ip6; > +} > + > +run_test( > + \&test_nic_start, > + "nic_start no IP, IPv4 subnet with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, > +]); > + > +run_test( > + \&test_nic_start, > + "nic_start already IP, IPv4 subnet with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }], > + "10.0.0.99" > +); > + > +run_test( > + \&test_nic_start, > + "nic_start already IP, IPv6 subnet with dhcp", > + [{ > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }], > + undef, > + "8888::99" > +); > + > +run_test( > + \&test_nic_start, > + "nic_start IP, IPv4+6 subnet with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }, > +]); > + > +run_test( > + \&test_nic_start, > + "nic_start already IPv4, IPv4+6 subnet with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }], > + "10.0.0.99" > +); > + > +run_test( > + \&test_nic_start, > + "nic_start already IPv6, IPv4+6 subnet with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }], > + undef, > + "8888::99" > +); > + > +run_test( > + \&test_nic_start, > + "nic_start already IPv4+6, IPv4+6 subnets with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + 'dhcp-range' =3D> ["start-address=3D8888::100,end-address=3D8888::200"]= , > + }], > + "10.0.0.99", > + "8888::99" > +); > + > +run_test( > + \&test_nic_start, > + "nic_start already IPv4+6, only IPv4 subnet with dhcp", > + [{ > + subnet =3D> "10.0.0.0/24", > + gateway =3D> "10.0.0.1", > + 'dhcp-range' =3D> ["start-address=3D10.0.0.100,end-address=3D10.0.0.200= "], > + }, { > + subnet =3D> "8888::/64", > + gateway =3D> "8888::1", > + }], > + "10.0.0.99", > + "8888::99" > +); > + > +done_testing();