8 use List
::Util
qw(first all);
9 use NetAddr
::IP
qw(:lower);
14 use PVE
::Tools
qw(extract_param file_set_contents);
16 use PVE
::Network
::SDN
;
17 use PVE
::Network
::SDN
::Zones
;
18 use PVE
::Network
::SDN
::Zones
::Plugin
;
19 use PVE
::Network
::SDN
::Controllers
;
20 use PVE
::Network
::SDN
::Dns
;
21 use PVE
::Network
::SDN
::Vnets
;
23 use PVE
::RESTEnvironment
;
25 use PVE
::API2
::Network
::SDN
::Zones
;
26 use PVE
::API2
::Network
::SDN
::Subnets
;
27 use PVE
::API2
::Network
::SDN
::Vnets
;
28 use PVE
::API2
::Network
::SDN
::Ipams
;
30 my $TMP_ETHERS_FILE = "/tmp/ethers";
32 my $test_state = undef;
33 sub clear_test_state
{
36 datacenter_config
=> {},
38 controller_config
=> {},
52 PVE
::Tools
::file_set_contents
($TMP_ETHERS_FILE, "\n");
56 my $mocked_cfs_lock_file = sub {
57 my ($filename, $timeout, $code, @param) = @_;
59 die "$filename already locked\n" if ($test_state->{locks
}->{$filename});
61 $test_state->{locks
}->{$filename} = 1;
63 my $res = eval { $code->(@param); };
65 delete $test_state->{locks
}->{$filename};
72 # Read structure back in again
73 open my $in, '<', $file or die $!;
76 local $/; # slurp mode
77 $sdn_config = eval <$in>;
84 $mocked_pve_sdn = Test
::MockModule-
>new('PVE::Network::SDN');
85 $mocked_pve_sdn->mock(
86 cfs_lock_file
=> $mocked_cfs_lock_file,
89 my $mocked_pve_tools = Test
::MockModule-
>new('PVE::Tools');
90 $mocked_pve_tools->mock(
91 lock_file
=> $mocked_cfs_lock_file,
95 $mocked_sdn_zones = Test
::MockModule-
>new('PVE::Network::SDN::Zones');
96 $mocked_sdn_zones->mock(
98 return $test_state->{zones_config
};
100 write_config
=> sub {
102 $test_state->{zones_config
} = $cfg;
106 my $mocked_sdn_zones_super_plugin;
107 $mocked_sdn_zones_super_plugin = Test
::MockModule-
>new('PVE::Network::SDN::Zones::Plugin');
108 $mocked_sdn_zones_super_plugin->mock(
109 datacenter_config
=> sub {
110 return $test_state->{datacenter_config
};
114 my $mocked_sdn_vnets;
115 $mocked_sdn_vnets = Test
::MockModule-
>new('PVE::Network::SDN::Vnets');
116 $mocked_sdn_vnets->mock(
118 return $test_state->{vnets_config
};
120 write_config
=> sub {
122 $test_state->{vnets_config
} = $cfg;
124 cfs_lock_file
=> $mocked_cfs_lock_file,
127 my $mocked_sdn_subnets;
128 $mocked_sdn_subnets = Test
::MockModule-
>new('PVE::Network::SDN::Subnets');
129 $mocked_sdn_subnets->mock(
131 return $test_state->{subnets_config
};
133 write_config
=> sub {
135 $test_state->{subnets_config
} = $cfg;
137 cfs_lock_file
=> $mocked_cfs_lock_file,
140 my $mocked_sdn_controller;
141 $mocked_sdn_controller = Test
::MockModule-
>new('PVE::Network::SDN::Controllers');
142 $mocked_sdn_controller->mock(
144 return $test_state->{controller_config
};
146 write_config
=> sub {
148 $test_state->{controller_config
} = $cfg;
150 cfs_lock_file
=> $mocked_cfs_lock_file,
155 $mocked_sdn_dns = Test
::MockModule-
>new('PVE::Network::SDN::Dns');
156 $mocked_sdn_dns->mock(
158 return $test_state->{dns_config
};
160 write_config
=> sub {
162 $test_state->{dns_config
} = $cfg;
164 cfs_lock_file
=> $mocked_cfs_lock_file,
168 my $mocked_sdn_ipams;
169 $mocked_sdn_ipams = Test
::MockModule-
>new('PVE::Network::SDN::Ipams');
170 $mocked_sdn_ipams->mock(
172 return $test_state->{ipam_config
};
174 write_config
=> sub {
176 $test_state->{ipam_config
} = $cfg;
179 return $test_state->{macdb
};
183 $test_state->{macdb
} = $cfg;
185 cfs_lock_file
=> $mocked_cfs_lock_file,
188 my $ipam_plugin = PVE
::Network
::SDN
::Ipams
::Plugin-
>lookup("pve"); # NOTE this is hard-coded to pve
189 my $mocked_ipam_plugin = Test
::MockModule-
>new($ipam_plugin);
190 $mocked_ipam_plugin->mock(
192 return $test_state->{ipamdb
};
196 $test_state->{ipamdb
} = $cfg;
198 cfs_lock_file
=> $mocked_cfs_lock_file,
201 my $mocked_sdn_dhcp_dnsmasq = Test
::MockModule-
>new('PVE::Network::SDN::Dhcp::Dnsmasq');
202 $mocked_sdn_dhcp_dnsmasq->mock(
203 assert_dnsmasq_installed
=> sub { return 1; },
204 before_configure
=> sub {},
205 ethers_file
=> sub { return "/tmp/ethers"; },
206 systemctl_service
=> sub {},
207 update_lease
=> sub {},
210 my $mocked_api_zones = Test
::MockModule-
>new('PVE::API2::Network::SDN::Zones');
211 $mocked_api_zones->mock(
212 create_etc_interfaces_sdn_dir
=> sub {},
215 my $rpcenv = PVE
::RESTEnvironment-
>init('priv');
216 $rpcenv->init_request();
217 $rpcenv->set_language("en_US.UTF-8");
218 $rpcenv->set_user('root@pam');
220 my $mocked_rpc_env_obj = Test
::MockModule-
>new('PVE::RESTEnvironment');
221 $mocked_rpc_env_obj->mock(
222 check_any
=> sub { return 1; },
225 my $mocked_pve_cluster_obj = Test
::MockModule-
>new('PVE::Cluster');
226 $mocked_pve_cluster_obj->mock(
227 check_cfs_quorum
=> sub { return 1; },
230 # ------- TEST FUNCTIONS --------------
233 my ($vnetid, $mac, $hostname, $vmid) = @_;
234 return PVE
::Network
::SDN
::Vnets
::add_next_free_cidr
($vnetid, $hostname, $mac, "$vmid", undef, 1);
238 my ($vnetid, $mac, $hostname) = @_;
239 return PVE
::Network
::SDN
::Vnets
::del_ips_from_mac
($vnetid, $mac, $hostname);
243 my ($vnetid, $mac, $vmid, $hostname) = @_;
244 return PVE
::Network
::SDN
::Vnets
::add_dhcp_mapping
($vnetid, $mac, $vmid, $hostname);
248 # ---- API HELPER FUNCTIONS FOR THE TESTS -----
253 return eval { PVE
::API2
::Network
::SDN
::Zones-
>read({zone
=> $id}); };
255 # verify get_zone actually fails if invalid
256 $t_invalid = get_zone
("invalid");
257 die("getting an invalid zone must fail") if (!$@);
258 fail
("getting an invalid zone must fail") if (defined $t_invalid);
262 my $zoneid = $params->{zone
};
264 eval { PVE
::API2
::Network
::SDN
::Zones-
>create($params); };
265 die("creating zone failed: $@") if ($@);
267 my $zone = get_zone
($zoneid);
268 die ("test setup: zone ($zoneid) not defined") if (!defined $zone);
274 return eval { PVE
::API2
::Network
::SDN
::Vnets-
>read({vnet
=> $id}); };
277 $t_invalid = get_vnet
("invalid");
278 die("getting an invalid vnet must fail") if (!$@);
279 fail
("getting an invalid vnet must fail") if (defined $t_invalid);
283 my $vnetid = $params->{vnet
};
284 PVE
::API2
::Network
::SDN
::Vnets-
>create($params);
286 my $vnet = get_vnet
($vnetid);
287 die ("test setup: vnet ($vnetid) not defined") if (!defined $vnet);
293 return eval { PVE
::API2
::Network
::SDN
::Subnets-
>read({subnet
=> $id}); };
296 $t_invalid = get_subnet
("invalid");
297 die("getting an invalid subnet must fail") if (!$@);
298 fail
("getting an invalid subnet must fail") if (defined $t_invalid);
302 PVE
::API2
::Network
::SDN
::Subnets-
>create($params);
305 sub get_ipam_entries
{
306 return PVE
::API2
::Network
::SDN
::Ipams-
>ipamindex({ipam
=> "pve"});
311 return PVE
::API2
::Network
::SDN
::Ips-
>ipcreate($param);
320 sub get_ips_from_mac
{
322 my $ipam_entries = get_ipam_entries
();
323 return grep { $_->{mac
} eq $mac if defined $_->{mac
} } $ipam_entries->@* if $ipam_entries;
327 my $ip4 = first
{ Net
::IP
::ip_is_ipv4
($_->{ip
}) } @_;
328 return $ip4->{ip
} if defined $ip4;
332 my $ip6 = first
{ Net
::IP
::ip_is_ipv6
($_->{ip
}) } @_;
333 return $ip6->{ip
} if defined $ip6;
337 # -------------- ACTUAL TESTS -----------------------
339 sub test_create_vnet_with_gateway
{
340 my $test_name = (split(/::/,(caller(0))[3]))[-1];
341 my $zoneid = "TESTZONE";
342 my $vnetid = "testvnet";
344 my $zone = create_zone
({
351 my $vnet = create_vnet
({
360 subnet
=> "10.0.0.0/24",
361 gateway
=> "10.0.0.1",
362 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
365 my ($p) = first
{ $_->{gateway
} == 1 } get_ipam_entries
()->@*;
366 ok
($p, "$test_name: Gateway IP was created in IPAM");
368 run_test
(\
&test_create_vnet_with_gateway
);
371 sub test_without_subnet
{
372 my $test_name = (split(/::/,(caller(0))[3]))[-1];
374 my $zoneid = "TESTZONE";
375 my $vnetid = "testvnet";
377 my $zone = create_zone
({
384 my $vnet = create_vnet
({
390 my $hostname = "testhostname";
391 my $mac = "da:65:8f:18:9b:6f";
395 nic_join
($vnetid, $mac, $hostname, $vmid);
399 fail
("$test_name: $@");
403 my @ips = get_ips_from_mac
($mac);
404 my $num_ips = scalar @ips;
405 is ($num_ips, 0, "$test_name: No IP allocated in IPAM");
407 run_test
(\
&test_without_subnet
);
411 my ($test_name, $subnets) = @_;
413 die "$test_name: we're expecting an array of subnets" if !$subnets;
414 my $num_subnets = scalar $subnets->@*;
415 die "$test_name: we're expecting an array of subnets. $num_subnets elements found" if ($num_subnets < 1);
416 my $num_dhcp_ranges = scalar grep { $_->{'dhcp-range'} } $subnets->@*;
418 my $zoneid = "TESTZONE";
419 my $vnetid = "testvnet";
421 my $zone = create_zone
({
428 my $vnet = create_vnet
({
434 foreach my $subnet ($subnets->@*) {
435 $subnet->{type
} = "subnet";
436 $subnet->{vnet
} = $vnetid;
437 create_subnet
($subnet);
440 my $hostname = "testhostname";
441 my $mac = "da:65:8f:18:9b:6f";
445 nic_join
($vnetid, $mac, $hostname, $vmid);
449 fail
("$test_name: $@");
453 my @ips = get_ips_from_mac
($mac);
454 my $num_ips = scalar @ips;
455 is ($num_ips, $num_dhcp_ranges, "$test_name: Expecting $num_dhcp_ranges IPs, found $num_ips");
456 ok
((all
{ ($_->{vnet
} eq $vnetid && $_->{zone
} eq $zoneid) } @ips),
457 "$test_name: all IPs in correct vnet and zone"
463 "nic_join IPv4 no dhcp",
465 subnet
=> "10.0.0.0/24",
466 gateway
=> "10.0.0.1",
472 "nic_join IPv6 no dhcp",
474 subnet
=> "8888::/64",
475 gateway
=> "8888::1",
481 "nic_join IPv4+6 no dhcp",
483 subnet
=> "10.0.0.0/24",
484 gateway
=> "10.0.0.1",
486 subnet
=> "8888::/64",
487 gateway
=> "8888::1",
493 "nic_join IPv4 with dhcp",
495 subnet
=> "10.0.0.0/24",
496 gateway
=> "10.0.0.1",
497 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
503 "nic_join IPv6 with dhcp",
505 subnet
=> "8888::/64",
506 gateway
=> "8888::1",
507 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
513 "nic_join IPv4+6 with dhcp",
515 subnet
=> "10.0.0.0/24",
516 gateway
=> "10.0.0.1",
517 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
519 subnet
=> "8888::/64",
520 gateway
=> "8888::1",
521 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
527 "nic_join IPv4 no DHCP, IPv6 with DHCP",
529 subnet
=> "10.0.0.0/24",
530 gateway
=> "10.0.0.1",
532 subnet
=> "8888::/64",
533 gateway
=> "8888::1",
534 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
540 "nic_join IPv4 with DHCP, IPv6 no DHCP",
542 subnet
=> "10.0.0.0/24",
543 gateway
=> "10.0.0.1",
544 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
546 subnet
=> "8888::/64",
547 gateway
=> "8888::1",
552 sub test_nic_join_full_dhcp_range
{
553 my ($test_name, $subnets, $expected_ip4, $expected_ip6) = @_;
555 die "$test_name: we're expecting an array of subnets" if !$subnets;
556 my $num_subnets = scalar $subnets->@*;
557 die "$test_name: we're expecting an array of subnets. $num_subnets elements found" if ($num_subnets < 1);
559 my $zoneid = "TESTZONE";
560 my $vnetid = "testvnet";
562 my $zone = create_zone
({
569 my $vnet = create_vnet
({
575 foreach my $subnet ($subnets->@*) {
576 $subnet->{type
} = "subnet";
577 $subnet->{vnet
} = $vnetid;
578 create_subnet
($subnet);
581 my $hostname = "testhostname";
582 my $mac = "da:65:8f:18:9b:6f";
586 nic_join
($vnetid, $mac, $hostname, $vmid);
590 fail
("$test_name: nic_join() is expected to fail because we cannot allocate all IPs");
593 my @ips = get_ips_from_mac
($mac);
594 my $num_ips = scalar @ips;
595 is ($num_ips, 0, "$test_name: No IP allocated in IPAM");
599 \
&test_nic_join_full_dhcp_range
,
600 "nic_join IPv4 with DHCP, dhcp-range full",
602 subnet
=> "10.0.0.0/24",
603 gateway
=> "10.0.0.100", # the gateway uses the only available IP in the dhcp-range
604 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.100"],
609 \
&test_nic_join_full_dhcp_range
,
610 "nic_join IPv6 with DHCP, dhcp-range full",
612 subnet
=> "8888::/64",
613 gateway
=> "8888::100", # the gateway uses the only available IP in the dhcp-range
614 'dhcp-range' => ["start-address=8888::100,end-address=8888::100"],
619 \
&test_nic_join_full_dhcp_range
,
620 "nic_join IPv4+6 with DHCP, dhcp-range full for both",
622 subnet
=> "10.0.0.0/24",
623 gateway
=> "10.0.0.100",
624 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.100"],
626 subnet
=> "8888::/64",
627 gateway
=> "8888::100",
628 'dhcp-range' => ["start-address=8888::100,end-address=8888::100"],
633 \
&test_nic_join_full_dhcp_range
,
634 "nic_join IPv4+6 with DHCP, dhcp-range full for IPv4",
636 subnet
=> "10.0.0.0/24",
637 gateway
=> "10.0.0.100", # the gateway uses the only available IP in the dhcp-range
638 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.100"],
640 subnet
=> "8888::/64",
641 gateway
=> "8888::1",
642 'dhcp-range' => ["start-address=8888::100,end-address=8888::100"],
647 \
&test_nic_join_full_dhcp_range
,
648 "nic_join IPv4+6 with DHCP, dhcp-range full for IPv6",
650 subnet
=> "10.0.0.0/24",
651 gateway
=> "10.0.0.1",
652 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.100"],
654 subnet
=> "8888::/64",
655 gateway
=> "8888::100",
656 'dhcp-range' => ["start-address=8888::100,end-address=8888::100"],
661 \
&test_nic_join_full_dhcp_range
,
662 "nic_join IPv4 no DHCP, dhcp-range full for IPv6",
664 subnet
=> "10.0.0.0/24",
665 gateway
=> "10.0.0.1",
667 subnet
=> "8888::/64",
668 gateway
=> "8888::100",
669 'dhcp-range' => ["start-address=8888::100,end-address=8888::100"],
674 # -------------- nic_start
676 my ($test_name, $subnets, $current_ip4, $current_ip6, $num_expected_ips) = @_;
678 die "$test_name: we're expecting an array of subnets" if !$subnets;
679 my $num_subnets = scalar $subnets->@*;
680 die "$test_name: we're expecting an array of subnets. $num_subnets elements found" if ($num_subnets < 1);
682 $num_expected_ips = scalar grep { $_->{'dhcp-range'} } $subnets->@* if !defined $num_expected_ips;
684 my $zoneid = "TESTZONE";
685 my $vnetid = "testvnet";
687 my $zone = create_zone
({
694 my $vnet = create_vnet
({
700 foreach my $subnet ($subnets->@*) {
701 $subnet->{type
} = "subnet";
702 $subnet->{vnet
} = $vnetid;
703 create_subnet
($subnet);
706 my $hostname = "testhostname";
707 my $mac = "da:65:8f:18:9b:6f";
727 my @current_ips = get_ips_from_mac
($mac);
728 is ( get_ip4
(@current_ips), $current_ip4, "$test_name: setup current IPv4: $current_ip4" ) if defined $current_ip4;
729 is ( get_ip6
(@current_ips), $current_ip6, "$test_name: setup current IPv6: $current_ip6" ) if defined $current_ip6;
732 nic_start
($vnetid, $mac, $hostname, $vmid);
736 fail
("$test_name: $@");
740 my @ips = get_ips_from_mac
($mac);
741 my $num_ips = scalar @ips;
742 is ($num_ips, $num_expected_ips, "$test_name: Expecting $num_expected_ips IPs, found $num_ips");
743 ok
((all
{ ($_->{vnet
} eq $vnetid && $_->{zone
} eq $zoneid) } @ips),
744 "$test_name: all IPs in correct vnet and zone"
747 is ( get_ip4
(@ips), $current_ip4, "$test_name: still current IPv4: $current_ip4" ) if $current_ip4;
748 is ( get_ip6
(@ips), $current_ip6, "$test_name: still current IPv6: $current_ip6" ) if $current_ip6;
753 "nic_start no IP, IPv4 without dhcp",
755 subnet
=> "10.0.0.0/24",
756 gateway
=> "10.0.0.1",
762 "nic_start already IP, IPv4 without dhcp",
764 subnet
=> "10.0.0.0/24",
765 gateway
=> "10.0.0.1",
774 "nic_start already IPv6, IPv6 without dhcp",
776 subnet
=> "8888::/64",
777 gateway
=> "8888::1",
786 "nic_start no IP, IPv4 subnet with dhcp",
788 subnet
=> "10.0.0.0/24",
789 gateway
=> "10.0.0.1",
790 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
796 "nic_start already IP, IPv4 subnet with dhcp",
798 subnet
=> "10.0.0.0/24",
799 gateway
=> "10.0.0.1",
800 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
807 "nic_start already IP, IPv6 subnet with dhcp",
809 subnet
=> "8888::/64",
810 gateway
=> "8888::1",
811 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
819 "nic_start IP, IPv4+6 subnet with dhcp",
821 subnet
=> "10.0.0.0/24",
822 gateway
=> "10.0.0.1",
823 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
825 subnet
=> "8888::/64",
826 gateway
=> "8888::1",
827 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
833 "nic_start already IPv4, IPv4+6 subnet with dhcp",
835 subnet
=> "10.0.0.0/24",
836 gateway
=> "10.0.0.1",
837 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
839 subnet
=> "8888::/64",
840 gateway
=> "8888::1",
841 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
848 "nic_start already IPv6, IPv4+6 subnet with dhcp",
850 subnet
=> "10.0.0.0/24",
851 gateway
=> "10.0.0.1",
852 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
854 subnet
=> "8888::/64",
855 gateway
=> "8888::1",
856 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
864 "nic_start already IPv4+6, IPv4+6 subnets with dhcp",
866 subnet
=> "10.0.0.0/24",
867 gateway
=> "10.0.0.1",
868 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
870 subnet
=> "8888::/64",
871 gateway
=> "8888::1",
872 'dhcp-range' => ["start-address=8888::100,end-address=8888::200"],
880 "nic_start already IPv4+6, only IPv4 subnet with dhcp",
882 subnet
=> "10.0.0.0/24",
883 gateway
=> "10.0.0.1",
884 'dhcp-range' => ["start-address=10.0.0.100,end-address=10.0.0.200"],
886 subnet
=> "8888::/64",
887 gateway
=> "8888::1",