@@ -492,6 +492,201 @@ def fail_saving_ips(ips, fail_error)
492492 it_behaves_like :retries_on_race_condition
493493 end
494494 end
495+
496+ context 'when handling CIDR blocks and overlapping ranges' do
497+ def save_ip_string ( ip_string )
498+ ip_addr = to_ipaddr ( ip_string )
499+ Bosh ::Director ::Models ::IpAddress . new (
500+ address_str : ip_addr . to_s ,
501+ network_name : 'my-manual-network' ,
502+ instance : instance_model ,
503+ task_id : Bosh ::Director ::Config . current_job . task_id
504+ ) . save
505+ end
506+
507+ context 'when database has individual IPs that are contained in a reserved CIDR block' do
508+ it 'deduplicates and skips the entire CIDR block' do
509+ network_spec [ 'subnets' ] . first [ 'range' ] = '10.0.11.32/27'
510+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '10.0.11.33'
511+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [ '10.0.11.32 - 10.0.11.35' , '10.0.11.63' ]
512+ network_spec [ 'subnets' ] . first [ 'static' ] = [ '10.0.11.36' , '10.0.11.37' , '10.0.11.38' , '10.0.11.39' , '10.0.11.40' ]
513+
514+ save_ip_string ( '10.0.11.32/32' )
515+ save_ip_string ( '10.0.11.33/32' )
516+
517+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
518+ expect ( ip_address ) . to eq ( cidr_ip ( '10.0.11.41' ) )
519+ end
520+ end
521+
522+ context 'when multiple overlapping CIDR blocks exist' do
523+ it 'deduplicates to largest block only' do
524+ network_spec [ 'subnets' ] . first [ 'range' ] = '192.168.1.0/24'
525+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '192.168.1.1'
526+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [
527+ '192.168.1.0 - 192.168.1.15' ,
528+ '192.168.1.0 - 192.168.1.3' ,
529+ '192.168.1.4 - 192.168.1.7' ,
530+ '192.168.1.8' ,
531+ ]
532+
533+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
534+ expect ( ip_address ) . to eq ( cidr_ip ( '192.168.1.16' ) )
535+ end
536+ end
537+
538+ context 'when nested CIDR blocks exist' do
539+ it 'deduplicates to outermost block' do
540+ network_spec [ 'subnets' ] . first [ 'range' ] = '192.168.1.0/24'
541+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '192.168.1.1'
542+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [
543+ '192.168.1.0/24' ,
544+ '192.168.1.0/26' ,
545+ '192.168.1.0/28' ,
546+ ]
547+
548+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
549+ expect ( ip_address ) . to be_nil
550+ end
551+ end
552+
553+ context 'when adjacent non-overlapping CIDR blocks exist' do
554+ it 'preserves all blocks and skips each correctly' do
555+ network_spec [ 'subnets' ] . first [ 'range' ] = '10.0.0.0/24'
556+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '10.0.0.1'
557+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [
558+ '10.0.0.0 - 10.0.0.3' ,
559+ '10.0.0.4 - 10.0.0.7' ,
560+ '10.0.0.8 - 10.0.0.11' ,
561+ ]
562+
563+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
564+ expect ( ip_address ) . to eq ( cidr_ip ( '10.0.0.12' ) )
565+ end
566+ end
567+
568+ context 'when large CIDR block contains scattered individual IPs' do
569+ it 'deduplicates scattered IPs within the block' do
570+ network_spec [ 'subnets' ] . first [ 'range' ] = '10.1.1.0/24'
571+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '10.1.1.1'
572+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [ '10.1.1.0/24' ]
573+ network_spec [ 'subnets' ] . first [ 'static' ] = [ ]
574+
575+ save_ip_string ( '10.1.1.5/32' )
576+ save_ip_string ( '10.1.1.50/32' )
577+ save_ip_string ( '10.1.1.100/32' )
578+ save_ip_string ( '10.1.1.200/32' )
579+
580+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
581+ expect ( ip_address ) . to be_nil
582+ end
583+ end
584+
585+ context 'when handling AWS reserved IP ranges' do
586+ it 'correctly skips reserved ranges with database IPs' do
587+ network_spec [ 'subnets' ] . first [ 'range' ] = '10.0.11.32/27'
588+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '10.0.11.33'
589+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [ '10.0.11.32 - 10.0.11.35' , '10.0.11.63' ]
590+ network_spec [ 'subnets' ] . first [ 'static' ] = [ ]
591+
592+ save_ip_string ( '10.0.11.32/32' )
593+ save_ip_string ( '10.0.11.33/32' )
594+ save_ip_string ( '10.0.11.34/32' )
595+
596+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
597+ expect ( ip_address ) . to eq ( cidr_ip ( '10.0.11.36' ) )
598+ end
599+ end
600+
601+ context 'when candidate block lands inside a reserved CIDR that started before it' do
602+ it 'does not allocate an address inside the reserved range' do
603+ # Reserved: 10.0.0.130/23 covers 10.0.130.0 - 10.0.131.255 (512 IPs).
604+ # Walking forward from 10.0.129.0/24: that first candidate overlaps
605+ # the /23, so we advance by 512, landing at 10.0.131.0. But .131.0
606+ # is still inside the /23. The pruning step must NOT discard the /23
607+ # just because its base (.130.0) < new candidate base (.131.0).
608+ # The first address outside the reserved range is 10.0.132.0.
609+ network_spec [ 'subnets' ] . first [ 'range' ] = '10.0.128.0/21'
610+ network_spec [ 'subnets' ] . first [ 'gateway' ] = '10.0.128.1'
611+ network_spec [ 'subnets' ] . first [ 'reserved' ] = [
612+ '10.0.128.0 - 10.0.129.255' ,
613+ '10.0.130.0 - 10.0.131.255' ,
614+ '10.0.132.0 - 10.0.133.0' ,
615+ ]
616+ network_spec [ 'subnets' ] . first [ 'static' ] = [ ]
617+
618+ ip_address = ip_repo . allocate_dynamic_ip ( reservation , subnet )
619+ expect ( ip_address ) . to eq ( cidr_ip ( '10.0.133.1' ) )
620+ end
621+ end
622+ end
623+
624+ context 'when allocating dynamic IPs from an IPv6 subnet' do
625+ let ( :ipv6_network_spec ) do
626+ {
627+ 'name' => 'my-manual-network' ,
628+ 'subnets' => [
629+ {
630+ 'range' => '2001:db8::/120' ,
631+ 'gateway' => '2001:db8::1' ,
632+ 'dns' => [ ] ,
633+ 'static' => [ ] ,
634+ 'reserved' => [ ] ,
635+ 'cloud_properties' => { } ,
636+ 'az' => 'az-1' ,
637+ } ,
638+ ] ,
639+ }
640+ end
641+
642+ let ( :ipv6_network ) do
643+ ManualNetwork . parse (
644+ ipv6_network_spec ,
645+ availability_zones ,
646+ per_spec_logger ,
647+ )
648+ end
649+
650+ let ( :ipv6_subnet ) do
651+ ManualNetworkSubnet . parse (
652+ ipv6_network . name ,
653+ ipv6_network_spec [ 'subnets' ] . first ,
654+ availability_zones ,
655+ )
656+ end
657+
658+ let ( :ipv6_reservation ) { Bosh ::Director ::DesiredNetworkReservation . new_dynamic ( instance_model , ipv6_network ) }
659+
660+ it 'returns the first available IPv6 address' do
661+ ip_address = ip_repo . allocate_dynamic_ip ( ipv6_reservation , ipv6_subnet )
662+ expect ( ip_address ) . to eq ( cidr_ip ( '2001:db8::2' ) )
663+ end
664+
665+ it 'allocates sequential IPv6 addresses' do
666+ first = ip_repo . allocate_dynamic_ip ( ipv6_reservation , ipv6_subnet )
667+ second = ip_repo . allocate_dynamic_ip ( ipv6_reservation , ipv6_subnet )
668+ expect ( first ) . to eq ( cidr_ip ( '2001:db8::2' ) )
669+ expect ( second ) . to eq ( cidr_ip ( '2001:db8::3' ) )
670+ end
671+
672+ context 'when there are reserved IPv6 ranges' do
673+ it 'skips reserved addresses' do
674+ ipv6_network_spec [ 'subnets' ] . first [ 'reserved' ] = [ '2001:db8::2 - 2001:db8::4' ]
675+
676+ ip_address = ip_repo . allocate_dynamic_ip ( ipv6_reservation , ipv6_subnet )
677+ expect ( ip_address ) . to eq ( cidr_ip ( '2001:db8::5' ) )
678+ end
679+ end
680+
681+ context 'when there are reserved IPv6 CIDR blocks' do
682+ it 'skips the entire CIDR block' do
683+ ipv6_network_spec [ 'subnets' ] . first [ 'reserved' ] = [ '2001:db8::/124' ]
684+
685+ ip_address = ip_repo . allocate_dynamic_ip ( ipv6_reservation , ipv6_subnet )
686+ expect ( ip_address ) . to eq ( cidr_ip ( '2001:db8::10' ) )
687+ end
688+ end
689+ end
495690 end
496691
497692 describe :allocate_vip_ip do
0 commit comments