@@ -20,6 +20,7 @@ import (
2020 "encoding/base64"
2121 "fmt"
2222 "net/netip"
23+ "strings"
2324
2425 "github.com/pkg/errors"
2526 "github.com/vmware/govmomi/object"
@@ -53,6 +54,18 @@ import (
5354// VMService provdes API to interact with the VMs using govmomi.
5455type VMService struct {}
5556
57+ // ipamDeviceConfig aids and holds state for the process of parsing IPAM
58+ // addresses for a given device.
59+ type ipamDeviceConfig struct {
60+ DeviceIndex int
61+ IPAMAddresses []* ipamv1.IPAddress
62+ MACAddress string
63+ NetworkSpecGateway4 string
64+ IPAMConfigGateway4 string
65+ NetworkSpecGateway6 string
66+ IPAMConfigGateway6 string
67+ }
68+
5669// ReconcileVM makes sure that the VM is in the desired state by:
5770// 1. Creating the VM if it does not exist, then...
5871// 2. Updating the VM with the bootstrap data, such as the cloud-init meta and user data, before...
@@ -279,17 +292,8 @@ func (vms *VMService) reconcileNetworkStatus(ctx *virtualMachineContext) error {
279292func (vms * VMService ) reconcileIPAddressClaims (ctx * context.VMContext ) (bool , error ) {
280293 for devIdx , device := range ctx .VSphereVM .Spec .Network .Devices {
281294 for poolRefIdx , poolRef := range device .AddressesFromPools {
282- // check if claim exists
283- ipAddrClaim := & ipamv1.IPAddressClaim {}
284295 ipAddrClaimName := IPAddressClaimName (ctx .VSphereVM .Name , devIdx , poolRefIdx )
285- ipAddrClaimKey := apitypes.NamespacedName {
286- Namespace : ctx .VSphereVM .Namespace ,
287- Name : ipAddrClaimName ,
288- }
289- var err error
290- if err = ctx .Client .Get (ctx , ipAddrClaimKey , ipAddrClaim ); err != nil && ! apierrors .IsNotFound (err ) {
291- return false , err
292- }
296+ _ , err := getIPAddrClaim (ctx , ipAddrClaimName )
293297 if err == nil {
294298 ctx .Logger .V (5 ).Info ("IPAddressClaim found" , "name" , ipAddrClaimName )
295299 }
@@ -335,122 +339,241 @@ func createIPAddressClaim(ctx *context.VMContext, ipAddrClaimName string, poolRe
335339// expected to contain a valid IP, Prefix and Gateway.
336340func (vms * VMService ) reconcileIPAddresses (ctx * virtualMachineContext ) (bool , error ) {
337341 ctx .IPAMState = map [string ]infrav1.NetworkDeviceSpec {}
338- for devIdx , device := range ctx .VSphereVM .Spec .Network .Devices {
339- var ipAddrs []string
340- var gateway4 string
341- var gateway6 string
342-
343- //TODO: Break this up into smaller functions
344- for poolRefIdx := range device .AddressesFromPools {
345- // check if claim exists
346- ipAddrClaim := & ipamv1.IPAddressClaim {}
347- ipAddrClaimName := IPAddressClaimName (ctx .VSphereVM .Name , devIdx , poolRefIdx )
348- ipAddrClaimKey := apitypes.NamespacedName {
349- Namespace : ctx .VSphereVM .Namespace ,
350- Name : ipAddrClaimName ,
342+
343+ ipamDeviceConfigs , err := buildIPAMDeviceConfigs (ctx )
344+ if err != nil {
345+ return false , err
346+ }
347+
348+ var errs []error
349+ for _ , ipamDeviceConfig := range ipamDeviceConfigs {
350+ var addressWithPrefixes []netip.Prefix
351+ for _ , ipamAddress := range ipamDeviceConfig .IPAMAddresses {
352+ addressWithPrefix , err := parseAddressWithPrefix (ipamAddress )
353+ if err != nil {
354+ errs = append (errs , err )
355+ continue
356+ }
357+
358+ if slices .Contains (addressWithPrefixes , addressWithPrefix ) {
359+ errs = append (errs ,
360+ fmt .Errorf ("IPAddress %s/%s is a duplicate of another address: %q" ,
361+ ipamAddress .Namespace ,
362+ ipamAddress .Name ,
363+ addressWithPrefix ))
364+ continue
351365 }
352- var err error
353- ctx .Logger .V (5 ).Info ("fetching IPAddressClaim" , "name" , ipAddrClaimKey .String ())
354- if err = ctx .Client .Get (ctx , ipAddrClaimKey , ipAddrClaim ); err != nil && ! apierrors .IsNotFound (err ) {
366+
367+ gatewayAddr , err := parseGateway (ipamAddress , addressWithPrefix , ipamDeviceConfig )
368+ if err != nil {
369+ errs = append (errs , err )
370+ continue
371+ }
372+
373+ if gatewayAddr .Is4 () {
374+ ipamDeviceConfig .IPAMConfigGateway4 = ipamAddress .Spec .Gateway
375+ } else {
376+ ipamDeviceConfig .IPAMConfigGateway6 = ipamAddress .Spec .Gateway
377+ }
378+
379+ addressWithPrefixes = append (addressWithPrefixes , addressWithPrefix )
380+ }
381+
382+ if len (addressWithPrefixes ) > 0 {
383+ ctx .IPAMState [ipamDeviceConfig .MACAddress ] = infrav1.NetworkDeviceSpec {
384+ IPAddrs : prefixesAsStrings (addressWithPrefixes ),
385+ Gateway4 : ipamDeviceConfig .IPAMConfigGateway4 ,
386+ Gateway6 : ipamDeviceConfig .IPAMConfigGateway6 ,
387+ }
388+ }
389+ }
390+
391+ if len (errs ) > 0 {
392+ var msgs []string
393+ for _ , err := range errs {
394+ msgs = append (msgs , err .Error ())
395+ }
396+ msg := strings .Join (msgs , "\n " )
397+ conditions .MarkFalse (ctx .VSphereVM ,
398+ infrav1 .IPAddressClaimedCondition ,
399+ infrav1 .IPAddressInvalidReason ,
400+ clusterv1 .ConditionSeverityError ,
401+ msg )
402+ return false , errors .New (msg )
403+ }
404+
405+ if len (ctx .IPAMState ) > 0 {
406+ conditions .MarkTrue (ctx .VSphereVM , infrav1 .IPAddressClaimedCondition )
407+ }
408+
409+ return true , nil
410+ }
411+
412+ // prefixesAsStrings converts []netip.Prefix to []string.
413+ func prefixesAsStrings (prefixes []netip.Prefix ) []string {
414+ prefixSrings := make ([]string , 0 , len (prefixes ))
415+ for _ , prefix := range prefixes {
416+ prefixSrings = append (prefixSrings , prefix .String ())
417+ }
418+ return prefixSrings
419+ }
420+
421+ // parseAddressWithPrefix converts a *ipamv1.IPAddress to a string, e.g. '10.0.0.1/24'.
422+ func parseAddressWithPrefix (ipamAddress * ipamv1.IPAddress ) (netip.Prefix , error ) {
423+ addressWithPrefix := fmt .Sprintf ("%s/%d" , ipamAddress .Spec .Address , ipamAddress .Spec .Prefix )
424+ parsedPrefix , err := netip .ParsePrefix (addressWithPrefix )
425+ if err != nil {
426+ return netip.Prefix {}, fmt .Errorf ("IPAddress %s/%s has invalid ip address: %q" ,
427+ ipamAddress .Namespace ,
428+ ipamAddress .Name ,
429+ addressWithPrefix ,
430+ )
431+ }
432+
433+ return parsedPrefix , nil
434+ }
435+
436+ // parseGateway parses the gateway address on a ipamv1.IPAddress and ensures it
437+ // does not conflict with the gateway addresses parsed from other
438+ // ipamv1.IPAddresses on the current device. Gateway addresses must be the same
439+ // family as the address on the ipamv1.IPAddress. Gateway addresses of one
440+ // family must match the other addresses of the same family.
441+ func parseGateway (ipamAddress * ipamv1.IPAddress , addressWithPrefix netip.Prefix , ipamDeviceConfig ipamDeviceConfig ) (netip.Addr , error ) {
442+ gatewayAddr , err := netip .ParseAddr (ipamAddress .Spec .Gateway )
443+ if err != nil {
444+ return netip.Addr {}, fmt .Errorf ("IPAddress %s/%s has invalid gateway: %q" ,
445+ ipamAddress .Namespace ,
446+ ipamAddress .Name ,
447+ ipamAddress .Spec .Gateway ,
448+ )
449+ }
450+
451+ if addressWithPrefix .Addr ().Is4 () != gatewayAddr .Is4 () {
452+ return netip.Addr {}, fmt .Errorf ("IPAddress %s/%s has mismatched gateway and address IP families" ,
453+ ipamAddress .Namespace ,
454+ ipamAddress .Name ,
455+ )
456+ }
457+
458+ if gatewayAddr .Is4 () {
459+ if areGatewaysMismatched (ipamDeviceConfig .NetworkSpecGateway4 , ipamAddress .Spec .Gateway ) {
460+ return netip.Addr {}, fmt .Errorf ("the IPv4 Gateway for IPAddress %s does not match the Gateway4 already configured on device (index %d)" ,
461+ ipamAddress .Name ,
462+ ipamDeviceConfig .DeviceIndex ,
463+ )
464+ }
465+ if areGatewaysMismatched (ipamDeviceConfig .IPAMConfigGateway4 , ipamAddress .Spec .Gateway ) {
466+ return netip.Addr {}, fmt .Errorf ("the IPv4 IPAddresses assigned to the same device (index %d) do not have the same gateway" ,
467+ ipamDeviceConfig .DeviceIndex ,
468+ )
469+ }
470+ } else {
471+ if areGatewaysMismatched (ipamDeviceConfig .NetworkSpecGateway6 , ipamAddress .Spec .Gateway ) {
472+ return netip.Addr {}, fmt .Errorf ("the IPv6 Gateway for IPAddress %s does not match the Gateway6 already configured on device (index %d)" ,
473+ ipamAddress .Name ,
474+ ipamDeviceConfig .DeviceIndex ,
475+ )
476+ }
477+ if areGatewaysMismatched (ipamDeviceConfig .IPAMConfigGateway6 , ipamAddress .Spec .Gateway ) {
478+ return netip.Addr {}, fmt .Errorf ("the IPv6 IPAddresses assigned to the same device (index %d) do not have the same gateway" ,
479+ ipamDeviceConfig .DeviceIndex ,
480+ )
481+ }
482+ }
483+
484+ return gatewayAddr , nil
485+ }
486+
487+ // buildIPAMDeviceConfigs checks that all the IPAddressClaims have been
488+ // satisfied.
489+ // If each IPAddressClaim has an associated IPAddress, a slice of
490+ // ipamDeviceConfig is returned, one for each device with addressesFromPools.
491+ // If any of the IPAddressClaims do not have an associated IPAddress yet,
492+ // a false condition is set and an error is returned, effectively stopping the
493+ // current reconcilliation loop.
494+ func buildIPAMDeviceConfigs (ctx * virtualMachineContext ) ([]ipamDeviceConfig , error ) {
495+ boundClaims := 0
496+ totalClaims := 0
497+ ipamDeviceConfigs := []ipamDeviceConfig {}
498+ for devIdx , networkSpecDevice := range ctx .VSphereVM .Spec .Network .Devices {
499+ ipamDeviceConfig := ipamDeviceConfig {
500+ IPAMAddresses : []* ipamv1.IPAddress {},
501+ MACAddress : networkSpecDevice .MACAddr ,
502+ NetworkSpecGateway4 : networkSpecDevice .Gateway4 ,
503+ NetworkSpecGateway6 : networkSpecDevice .Gateway6 ,
504+ DeviceIndex : devIdx ,
505+ }
506+
507+ for poolRefIdx := range networkSpecDevice .AddressesFromPools {
508+ totalClaims ++
509+
510+ ipAddrClaimName := IPAddressClaimName (ctx .VSphereVM .Name , ipamDeviceConfig .DeviceIndex , poolRefIdx )
511+
512+ ipAddrClaim , err := getIPAddrClaim (& ctx .VMContext , ipAddrClaimName )
513+ if err != nil {
355514 ctx .Logger .Error (err , "error fetching IPAddressClaim" , "name" , ipAddrClaimName )
356- return false , err
515+ if apierrors .IsNotFound (err ) {
516+ // it would be odd for this to occur, a findorcreate just happened in a previous step
517+ continue
518+ }
519+ return nil , err
357520 }
358521
522+ ctx .Logger .V (5 ).Info ("fetched IPAddressClaim" , "name" , ipAddrClaimName , "namespace" , ctx .VSphereVM .Namespace )
523+
359524 ipAddrName := ipAddrClaim .Status .AddressRef .Name
360- ctx .Logger .V (5 ).Info ("fetched IPAddressClaim" , "name" , ipAddrClaimName , "IPAddressClaim.Status.AddressRef.Name" , ipAddrName )
361525 if ipAddrName == "" {
362- ctx .Logger .V (5 ).Info ("IPAddress name was empty on IPAddressClaim" , "name" , ipAddrClaimName , "IPAddressClaim.Status.AddressRef.Name" , ipAddrName )
363- msg := "Waiting for IPAddressClaim to have an IPAddress bound"
364- markIPAddressClaimedConditionWaitingForClaimAddress (ctx .VSphereVM , msg )
365- return false , errors .New (msg )
526+ ctx .Logger .V (5 ).Info ("IPAddress not yet bound to IPAddressClaim" , "name" , ipAddrClaimName , "namespace" , ctx .VSphereVM .Namespace )
527+ continue
366528 }
367529
368530 ipAddr := & ipamv1.IPAddress {}
369531 ipAddrKey := apitypes.NamespacedName {
370532 Namespace : ctx .VSphereVM .Namespace ,
371533 Name : ipAddrName ,
372534 }
373- if err = ctx .Client .Get (ctx , ipAddrKey , ipAddr ); err != nil {
374- return false , err
375- }
376535
377- toAdd := fmt .Sprintf ("%s/%d" , ipAddr .Spec .Address , ipAddr .Spec .Prefix )
378- parsedPrefix , err := netip .ParsePrefix (toAdd )
379- if err != nil {
380- msg := fmt .Sprintf ("IPAddress %s/%s has invalid ip address: %q" ,
381- ipAddrKey .Namespace ,
382- ipAddrKey .Name ,
383- toAdd ,
384- )
385- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
536+ if err := ctx .Client .Get (ctx , ipAddrKey , ipAddr ); err != nil {
537+ // because the ref was set on the claim, it is expected this error should not occur
538+ return nil , err
386539 }
387540
388- if ! slices .Contains (ipAddrs , toAdd ) {
389- ipAddrs = append (ipAddrs , toAdd )
390-
391- gatewayAddr , err := netip .ParseAddr (ipAddr .Spec .Gateway )
392- if err != nil {
393- msg := fmt .Sprintf ("IPAddress %s/%s has invalid gateway: %q" ,
394- ipAddrKey .Namespace ,
395- ipAddrKey .Name ,
396- ipAddr .Spec .Gateway ,
397- )
398- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
399- }
400-
401- if parsedPrefix .Addr ().Is4 () != gatewayAddr .Is4 () {
402- msg := fmt .Sprintf ("IPAddress %s/%s has mismatched gateway and address IP families" ,
403- ipAddrKey .Namespace ,
404- ipAddrKey .Name ,
405- )
406- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
407- }
408-
409- if gatewayAddr .Is4 () {
410- if device .Gateway4 != "" && device .Gateway4 != ipAddr .Spec .Gateway {
411- msg := fmt .Sprintf ("The IPv4 Gateway for IPAddress %s does not match the Gateway4 already configured on device (index %d)" ,
412- ipAddrName ,
413- devIdx ,
414- )
415- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
416- }
417- if gateway4 != "" && gateway4 != ipAddr .Spec .Gateway {
418- msg := fmt .Sprintf ("The IPv4 IPAddresses assigned to the same device (index %d) do not have the same gateway" ,
419- devIdx ,
420- )
421- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
422- }
423- gateway4 = ipAddr .Spec .Gateway
424- } else {
425- if device .Gateway6 != "" && device .Gateway6 != ipAddr .Spec .Gateway {
426- msg := fmt .Sprintf ("The IPv6 Gateway for IPAddress %s does not match the Gateway6 already configured on device (index %d)" ,
427- ipAddrName ,
428- devIdx ,
429- )
430- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
431- }
432- if gateway6 != "" && gateway6 != ipAddr .Spec .Gateway {
433- msg := fmt .Sprintf ("The IPv6 IPAddresses assigned to the same device (index %d) do not have the same gateway" ,
434- devIdx ,
435- )
436- return markIPAddressClaimedConditionInvalidIPWithError (ctx .VSphereVM , msg )
437- }
438- gateway6 = ipAddr .Spec .Gateway
439- }
440- }
441- ctx .IPAMState [device .MACAddr ] = infrav1.NetworkDeviceSpec {
442- IPAddrs : ipAddrs ,
443- Gateway4 : gateway4 ,
444- Gateway6 : gateway6 ,
445- }
541+ ipamDeviceConfig .IPAMAddresses = append (ipamDeviceConfig .IPAMAddresses , ipAddr )
542+ boundClaims ++
446543 }
544+ ipamDeviceConfigs = append (ipamDeviceConfigs , ipamDeviceConfig )
447545 }
448546
449- if len (ctx .IPAMState ) > 0 {
450- conditions .MarkTrue (ctx .VSphereVM , infrav1 .IPAddressClaimedCondition )
547+ if boundClaims < totalClaims {
548+ msg := fmt .Sprintf ("Waiting for IPAddressClaim to have an IPAddress bound, %d out of %d bound" , boundClaims , totalClaims )
549+ markIPAddressClaimedConditionWaitingForClaimAddress (ctx .VSphereVM , msg )
550+ return nil , errors .New (msg )
451551 }
452552
453- return true , nil
553+ return ipamDeviceConfigs , nil
554+ }
555+
556+ // areGatewaysMismatched checks that a gateway for a device is equal to an
557+ // IPAddresses gateway. We can assume that IPAddresses will always have
558+ // gateways so we do not need to check for empty string. It is possible to
559+ // configure a device and not a gateway, we don't want to fail in that case.
560+ func areGatewaysMismatched (deviceGateway , ipAddressGateway string ) bool {
561+ return deviceGateway != "" && deviceGateway != ipAddressGateway
562+ }
563+
564+ // getIPAddrClaim fetches an IPAddressClaim from the api with the given name.
565+ func getIPAddrClaim (ctx * context.VMContext , ipAddrClaimName string ) (* ipamv1.IPAddressClaim , error ) {
566+ ipAddrClaim := & ipamv1.IPAddressClaim {}
567+ ipAddrClaimKey := apitypes.NamespacedName {
568+ Namespace : ctx .VSphereVM .Namespace ,
569+ Name : ipAddrClaimName ,
570+ }
571+
572+ ctx .Logger .V (5 ).Info ("fetching IPAddressClaim" , "name" , ipAddrClaimKey .String ())
573+ if err := ctx .Client .Get (ctx , ipAddrClaimKey , ipAddrClaim ); err != nil {
574+ return nil , err
575+ }
576+ return ipAddrClaim , nil
454577}
455578
456579func (vms * VMService ) reconcileMetadata (ctx * virtualMachineContext ) (bool , error ) {
@@ -854,15 +977,6 @@ func (vms *VMService) reconcileClusterModuleMembership(ctx *virtualMachineContex
854977 return nil
855978}
856979
857- func markIPAddressClaimedConditionInvalidIPWithError (vm * infrav1.VSphereVM , msg string ) (bool , error ) {
858- conditions .MarkFalse (vm ,
859- infrav1 .IPAddressClaimedCondition ,
860- infrav1 .IPAddressInvalidReason ,
861- clusterv1 .ConditionSeverityError ,
862- msg )
863- return false , errors .New (msg )
864- }
865-
866980func markIPAddressClaimedConditionWaitingForClaimAddress (vm * infrav1.VSphereVM , msg string ) {
867981 conditions .MarkFalse (vm ,
868982 infrav1 .IPAddressClaimedCondition ,
0 commit comments