// Copyright (c) 2016-2019 Tigera, Inc. All rights reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package ipam import ( "context" "errors" "fmt" log "github.com/sirupsen/logrus" "github.com/projectcalico/libcalico-go/lib/apis/v3" "github.com/projectcalico/libcalico-go/lib/set" bapi "github.com/projectcalico/libcalico-go/lib/backend/api" "github.com/projectcalico/libcalico-go/lib/backend/model" cerrors "github.com/projectcalico/libcalico-go/lib/errors" "github.com/projectcalico/libcalico-go/lib/names" "github.com/projectcalico/libcalico-go/lib/net" ) const ( // Number of retries when we have an error writing data // to etcd. datastoreRetries = 100 ipamKeyErrRetries = 3 // Common attributes which may be set on allocations by clients. Moved to the model package so they can be used // by the AllocationBlock code too. AttributePod = model.IPAMBlockAttributePod AttributeNamespace = model.IPAMBlockAttributeNamespace AttributeNode = model.IPAMBlockAttributeNode AttributeType = model.IPAMBlockAttributeType AttributeTypeIPIP = model.IPAMBlockAttributeTypeIPIP AttributeTypeVXLAN = model.IPAMBlockAttributeTypeVXLAN ) var ( ErrBlockLimit = errors.New("cannot allocate new block due to per host block limit") ) // NewIPAMClient returns a new ipamClient, which implements Interface. // Consumers of the Calico API should not create this directly, but should // access IPAM through the main client IPAM accessor (e.g. clientv3.IPAM()) func NewIPAMClient(client bapi.Client, pools PoolAccessorInterface) Interface { return &ipamClient{ client: client, pools: pools, blockReaderWriter: blockReaderWriter{ client: client, pools: pools, }, } } // ipamClient implements Interface type ipamClient struct { client bapi.Client pools PoolAccessorInterface blockReaderWriter blockReaderWriter } // AutoAssign automatically assigns one or more IP addresses as specified by the // provided AutoAssignArgs. AutoAssign returns the list of the assigned IPv4 addresses, // and the list of the assigned IPv6 addresses. // // In case of error, returns the IPs allocated so far along with the error. func (c ipamClient) AutoAssign(ctx context.Context, args AutoAssignArgs) ([]net.IPNet, []net.IPNet, error) { // Determine the hostname to use - prefer the provided hostname if // non-nil, otherwise use the hostname reported by os. hostname, err := decideHostname(args.Hostname) if err != nil { return nil, nil, err } log.Infof("Auto-assign %d ipv4, %d ipv6 addrs for host '%s'", args.Num4, args.Num6, hostname) var v4list, v6list []net.IPNet if args.Num4 != 0 { // Assign IPv4 addresses. log.Debugf("Assigning IPv4 addresses") for _, pool := range args.IPv4Pools { if pool.IP.To4() == nil { return nil, nil, fmt.Errorf("provided IPv4 IPPools list contains one or more IPv6 IPPools") } } v4list, err = c.autoAssign(ctx, args.Num4, args.HandleID, args.Attrs, args.IPv4Pools, 4, hostname, args.MaxBlocksPerHost) if err != nil { log.Errorf("Error assigning IPV4 addresses: %v", err) return v4list, nil, err } } if args.Num6 != 0 { // If no err assigning V4, try to assign any V6. log.Debugf("Assigning IPv6 addresses") for _, pool := range args.IPv6Pools { if pool.IP.To4() != nil { return nil, nil, fmt.Errorf("provided IPv6 IPPools list contains one or more IPv4 IPPools") } } v6list, err = c.autoAssign(ctx, args.Num6, args.HandleID, args.Attrs, args.IPv6Pools, 6, hostname, args.MaxBlocksPerHost) if err != nil { log.Errorf("Error assigning IPV6 addresses: %v", err) return v4list, v6list, err } } return v4list, v6list, nil } // getBlockFromAffinity returns the block referenced by the given affinity, attempting to create it if // it does not exist. getBlockFromAffinity will delete the provided affinity if it does not match the actual // affinity of the block. func (c ipamClient) getBlockFromAffinity(ctx context.Context, aff *model.KVPair) (*model.KVPair, error) { // Parse out affinity data. cidr := aff.Key.(model.BlockAffinityKey).CIDR host := aff.Key.(model.BlockAffinityKey).Host state := aff.Value.(*model.BlockAffinity).State logCtx := log.WithFields(log.Fields{"host": host, "cidr": cidr}) // Get the block referenced by this affinity. logCtx.Info("Attempting to load block") b, err := c.blockReaderWriter.queryBlock(ctx, cidr, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // The block referenced by the affinity doesn't exist. Try to create it. logCtx.Info("The referenced block doesn't exist, trying to create it") aff.Value.(*model.BlockAffinity).State = model.StatePending aff, err = c.blockReaderWriter.updateAffinity(ctx, aff) if err != nil { logCtx.WithError(err).Warn("Error updating block affinity") return nil, err } logCtx.Info("Wrote affinity as pending") cfg, err := c.GetIPAMConfig(ctx) if err != nil { logCtx.WithError(err).Errorf("Error getting IPAM Config") return nil, err } // Claim the block, which will also confirm the affinity. logCtx.Info("Attempting to claim the block") b, err := c.blockReaderWriter.claimAffineBlock(ctx, aff, *cfg) if err != nil { logCtx.WithError(err).Warn("Error claiming block") return nil, err } return b, nil } logCtx.WithError(err).Error("Error getting block") return nil, err } // If the block doesn't match the affinity, it means we've got a stale affininty hanging around. // We should remove it. blockAffinity := b.Value.(*model.AllocationBlock).Affinity if blockAffinity == nil || *blockAffinity != fmt.Sprintf("host:%s", host) { logCtx.WithField("blockAffinity", blockAffinity).Warn("Block does not match the provided affinity, deleting stale affinity") err := c.blockReaderWriter.deleteAffinity(ctx, aff) if err != nil { logCtx.WithError(err).Warn("Error deleting stale affinity") return nil, err } return nil, errStaleAffinity(fmt.Sprintf("Affinity is stale: %+v", aff)) } // If the block does match the affinity but the affinity has not been confirmed, // try to confirm it. Treat empty string as confirmed for compatibility with older data. if state != model.StateConfirmed && state != "" { // Write the affinity as pending. logCtx.Info("Affinity has not been confirmed - attempt to confirm it") aff.Value.(*model.BlockAffinity).State = model.StatePending aff, err = c.blockReaderWriter.updateAffinity(ctx, aff) if err != nil { logCtx.WithError(err).Warn("Error marking affinity as pending as part of confirmation process") return nil, err } // CAS the block to get a new revision and invalidate any other instances // that might be trying to operate on the block. logCtx.Info("Writing block to get a new revision") b, err = c.blockReaderWriter.updateBlock(ctx, b) if err != nil { logCtx.WithError(err).Debug("Error writing block") return nil, err } // Confirm the affinity. logCtx.Info("Attempting to confirm affinity") aff.Value.(*model.BlockAffinity).State = model.StateConfirmed aff, err = c.blockReaderWriter.updateAffinity(ctx, aff) if err != nil { logCtx.WithError(err).Debug("Error confirming affinity") return nil, err } logCtx.Info("Affinity confirmed successfully") } logCtx.Info("Affinity is confirmed and block has been loaded") return b, nil } // determinePools compares a list of requested pools with the enabled pools and returns the intersect. // If any requested pool does not exist, or is not enabled, an error is returned. // If no pools are requested, all enabled pools are returned. // Also applies selector logic on node labels to determine if the pool is a match. // Returns the set of matching pools as well as the full set of ip pools. func (c ipamClient) determinePools(requestedPoolNets []net.IPNet, version int, node v3.Node) (matchingPools, enabledPools []v3.IPPool, err error) { // Get all the enabled IP pools from the datastore. enabledPools, err = c.pools.GetEnabledPools(version) if err != nil { log.WithError(err).Errorf("Error getting IP pools") return } log.Debugf("enabled pools: %v", enabledPools) log.Debugf("requested pools: %v", requestedPoolNets) // Build a map so we can lookup existing pools by their CIDR. pm := map[string]v3.IPPool{} for _, p := range enabledPools { pm[p.Spec.CIDR] = p } // Build a list of requested IP pool objects based on the provided CIDRs, validating // that each one actually exists and is enabled for IPAM. requestedPools := []v3.IPPool{} for _, rp := range requestedPoolNets { if pool, ok := pm[rp.String()]; !ok { // The requested pool doesn't exist. err = fmt.Errorf("the given pool (%s) does not exist, or is not enabled", rp.IPNet.String()) return } else { requestedPools = append(requestedPools, pool) } } // If requested IP pools are provided, use those unconditionally. We will ignore // IP pool selectors in this case. We need this for backwards compatibility, since IP pool // node selectors have not always existed. if len(requestedPools) > 0 { log.Debugf("Using the requested IP pools") matchingPools = requestedPools return } // At this point, we've determined the set of enabled IP pools which are valid for use. // We only want to use IP pools which actually match this node, so do a filter based on // selector. for _, pool := range enabledPools { var matches bool matches, err = pool.SelectsNode(node) if err != nil { log.WithError(err).WithField("pool", pool).Error("failed to determine if node matches pool") return } if !matches { // Do not consider pool enabled if the nodeSelector doesn't match the node's labels. log.Debugf("IP pool does not match this node: %s", pool.Name) continue } log.Debugf("IP pool matches this node: %s", pool.Name) matchingPools = append(matchingPools, pool) } return } func (c ipamClient) autoAssign(ctx context.Context, num int, handleID *string, attrs map[string]string, requestedPools []net.IPNet, version int, host string, maxNumBlocks int) ([]net.IPNet, error) { // Retrieve node for given hostname to use for ip pool node selection node, err := c.client.Get(ctx, model.ResourceKey{Kind: v3.KindNode, Name: host}, "") if err != nil { log.WithError(err).WithField("node", host).Error("failed to get node for host") return nil, err } // Make sure the returned value is OK. v3n, ok := node.Value.(*v3.Node) if !ok { return nil, fmt.Errorf("Datastore returned malformed node object") } // Determine the correct set of IP pools to use for this request. pools, allPools, err := c.determinePools(requestedPools, version, *v3n) if err != nil { return nil, err } // If there are no pools, we cannot assign addresses. if len(pools) == 0 { return nil, fmt.Errorf("no configured Calico pools for node %v", host) } // First, we try to assign addresses from one of the existing host-affine blocks. We // always do strict checking at this stage, so it doesn't matter whether // globally we have strict_affinity or not. logCtx := log.WithFields(log.Fields{"host": host}) if handleID != nil { logCtx = logCtx.WithField("handle", *handleID) } logCtx.Info("Looking up existing affinities for host") affBlocks, affBlocksToRelease, err := c.blockReaderWriter.getAffineBlocks(ctx, host, version, pools) if err != nil { return nil, err } // Release any emptied blocks still affine to this host but no longer part of an IP Pool which selects this node. for _, block := range affBlocksToRelease { // Determine the pool for each block. pool, err := c.blockReaderWriter.getPoolForIP(net.IP{block.IP}, allPools) if err != nil { log.WithError(err).Warnf("Failed to get pool for IP") continue } if pool == nil { logCtx.WithFields(log.Fields{"block": block}).Warn("No pool found for block, skipping") continue } // Determine if the pool selects the current node, refusing to release this particular block affinity if so. blockSelectsNode, err := pool.SelectsNode(*v3n) if err != nil { logCtx.WithError(err).WithField("pool", pool).Error("Failed to determine if node matches pool, skipping") continue } if blockSelectsNode { logCtx.WithFields(log.Fields{"pool": pool, "block": block}).Debug("Block's pool still selects node, refusing to remove affinity") continue } // Release the block affinity, requiring it to be empty. for i := 0; i < datastoreRetries; i++ { if err = c.blockReaderWriter.releaseBlockAffinity(ctx, host, block, true); err != nil { if _, ok := err.(errBlockClaimConflict); ok { // Not claimed by this host - ignore. } else if _, ok := err.(errBlockNotEmpty); ok { // Block isn't empty - ignore. } else if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // Block does not exist - ignore. } else { logCtx.WithError(err).WithField("block", block).Warn("Error occurred releasing block, trying again") continue } } logCtx.WithField("block", block).Info("Released affine block that no longer selects this host") break } } logCtx.Debugf("Found %d affine IPv%d blocks for host: %v", len(affBlocks), version, affBlocks) ips := []net.IPNet{} newIPs := []net.IPNet{} // Record how many blocks we own so we can check against the limit later. numBlocksOwned := len(affBlocks) for len(ips) < num { if len(affBlocks) == 0 { logCtx.Infof("Ran out of existing affine blocks for host") break } cidr := affBlocks[0] affBlocks = affBlocks[1:] // Try to assign from this block - if we hit a CAS error, we'll try this block again. // For any other error, we'll break out and try the next affine block. for i := 0; i < datastoreRetries; i++ { // Get the affinity. logCtx.Infof("Trying affinity for %s", cidr) aff, err := c.blockReaderWriter.queryAffinity(ctx, host, cidr, "") if err != nil { logCtx.WithError(err).Warnf("Error getting affinity") break } // Get the block which is referenced by the affinity, creating it if necessary. b, err := c.getBlockFromAffinity(ctx, aff) if err != nil { // Couldn't get a block for this affinity. if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error getting affine block - retry") continue } logCtx.WithError(err).Warn("Couldn't get block for affinity, try next one") break } // Assign IPs from the block. newIPs, err = c.assignFromExistingBlock(ctx, b, num, handleID, attrs, host, true) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error assigning from affine block - retry") continue } logCtx.WithError(err).Warn("Couldn't assign from affine block, try next one") break } ips = append(ips, newIPs...) break } logCtx.Infof("Block '%s' provided addresses: %v", cidr.String(), newIPs) } // If there are still addresses to allocate, then we've run out of // existing blocks with affinity to this host. Before we can assign new blocks or assign in // non-affine blocks, we need to check that our IPAM configuration // allows that. config, err := c.GetIPAMConfig(ctx) if err != nil { return ips, err } logCtx.Debugf("Allocate new blocks? Config: %+v", config) if config.AutoAllocateBlocks == true { rem := num - len(ips) retries := datastoreRetries for rem > 0 && retries > 0 { if maxNumBlocks > 0 && numBlocksOwned >= maxNumBlocks { log.Warnf("Unable to allocate a new IPAM block; host already has %v blocks but "+ "blocks per host limit is %v", numBlocksOwned, maxNumBlocks) return ips, ErrBlockLimit } // Claim a new block. logCtx.Infof("No more affine blocks, but need to allocate %d more addresses - allocate another block", rem) retries = retries - 1 // First, try to find an unclaimed block. logCtx.Info("Looking for an unclaimed block") subnet, err := c.blockReaderWriter.findUnclaimedBlock(ctx, host, version, pools, *config) if err != nil { if _, ok := err.(noFreeBlocksError); ok { // No free blocks. Break. logCtx.Info("No free blocks available for allocation") break } log.WithError(err).Error("Failed to find an unclaimed block") return ips, err } logCtx := log.WithFields(log.Fields{"host": host, "subnet": subnet}) logCtx.Info("Found unclaimed block") for i := 0; i < datastoreRetries; i++ { // We found an unclaimed block - claim affinity for it. pa, err := c.blockReaderWriter.getPendingAffinity(ctx, host, *subnet) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error claiming pending affinity, retry") continue } logCtx.WithError(err).Errorf("Error claiming pending affinity") return ips, err } // We have an affinity - try to get the block. b, err := c.getBlockFromAffinity(ctx, pa) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error getting block, retry") continue } else if _, ok := err.(errBlockClaimConflict); ok { logCtx.WithError(err).Debug("Block taken by someone else, find a new one") break } else if _, ok := err.(errStaleAffinity); ok { logCtx.WithError(err).Debug("Affinity is stale, find a new one") break } logCtx.WithError(err).Errorf("Error getting block for affinity") return ips, err } // Claim successful. Assign addresses from the new block. logCtx.Infof("Claimed new block %v - assigning %d addresses", b, rem) numBlocksOwned++ newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, config.StrictAffinity) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { log.WithError(err).Debug("CAS Error assigning from new block - retry") continue } logCtx.WithError(err).Warningf("Failed to assign IPs in newly allocated block") break } logCtx.Debugf("Assigned IPs from new block: %s", newIPs) ips = append(ips, newIPs...) rem = num - len(ips) break } } if retries == 0 { return ips, errors.New("Max retries hit - excessive concurrent IPAM requests") } } // If there are still addresses to allocate, we've now tried all blocks // with some affinity to us, and tried (and failed) to allocate new // ones. If we do not require strict host affinity, our last option is // a random hunt through any blocks we haven't yet tried. // // Note that this processing simply takes all of the IP pools and breaks // them up into block-sized CIDRs, then shuffles and searches through each // CIDR. This algorithm does not work if we disallow auto-allocation of // blocks because the allocated blocks may be sparsely populated in the // pools resulting in a very slow search for free addresses. // // If we need to support non-strict affinity and no auto-allocation of // blocks, then we should query the actual allocation blocks and assign // from those. rem := num - len(ips) if config.StrictAffinity != true && rem != 0 { logCtx.Infof("Attempting to assign %d more addresses from non-affine blocks", rem) // Iterate over pools and assign addresses until we either run out of pools, // or the request has been satisfied. logCtx.Info("Looking for blocks with free IP addresses") for _, p := range pools { logCtx.Debugf("Assigning from non-affine blocks in pool %s", p.Spec.CIDR) newBlock := randomBlockGenerator(p, host) for rem > 0 { // Grab a new random block. blockCIDR := newBlock() if blockCIDR == nil { logCtx.Warningf("All addresses exhausted in pool %s", p.Spec.CIDR) break } for i := 0; i < datastoreRetries; i++ { b, err := c.blockReaderWriter.queryBlock(ctx, *blockCIDR, "") if err != nil { logCtx.WithError(err).Warn("Failed to get non-affine block") break } // Attempt to assign from the block. logCtx.Infof("Attempting to assign IPs from non-affine block %s", blockCIDR.String()) newIPs, err := c.assignFromExistingBlock(ctx, b, rem, handleID, attrs, host, false) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error assigning from non-affine block - retry") continue } logCtx.WithError(err).Warningf("Failed to assign IPs from non-affine block in pool %s", p.Spec.CIDR) break } if len(newIPs) == 0 { break } logCtx.Infof("Successfully assigned IPs from non-affine block %s", blockCIDR.String()) ips = append(ips, newIPs...) rem = num - len(ips) break } } } } logCtx.Infof("Auto-assigned %d out of %d IPv%ds: %v", len(ips), num, version, ips) return ips, nil } // AssignIP assigns the provided IP address to the provided host. The IP address // must fall within a configured pool. AssignIP will claim block affinity as needed // in order to satisfy the assignment. An error will be returned if the IP address // is already assigned, or if StrictAffinity is enabled and the address is within // a block that does not have affinity for the given host. func (c ipamClient) AssignIP(ctx context.Context, args AssignIPArgs) error { hostname, err := decideHostname(args.Hostname) if err != nil { return err } log.Infof("Assigning IP %s to host: %s", args.IP, hostname) pool, err := c.blockReaderWriter.getPoolForIP(args.IP, nil) if err != nil { return err } if pool == nil { return errors.New("The provided IP address is not in a configured pool\n") } blockCIDR := getBlockCIDRForAddress(args.IP, pool) log.Debugf("IP %s is in block '%s'", args.IP.String(), blockCIDR.String()) for i := 0; i < datastoreRetries; i++ { obj, err := c.blockReaderWriter.queryBlock(ctx, blockCIDR, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok { log.WithError(err).Error("Error getting block") return err } log.Debugf("Block for IP %s does not yet exist, creating", args.IP) cfg, err := c.GetIPAMConfig(ctx) if err != nil { log.Errorf("Error getting IPAM Config: %v", err) return err } pa, err := c.blockReaderWriter.getPendingAffinity(ctx, hostname, blockCIDR) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { log.WithError(err).Debug("CAS error claiming affinity for block - retry") continue } return err } obj, err = c.blockReaderWriter.claimAffineBlock(ctx, pa, *cfg) if err != nil { if _, ok := err.(*errBlockClaimConflict); ok { log.Warningf("Someone else claimed block %s before us", blockCIDR.String()) continue } else if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { log.WithError(err).Debug("CAS error claiming affine block - retry") continue } log.WithError(err).Error("Error claiming block") return err } log.Infof("Claimed new block: %s", blockCIDR) } block := allocationBlock{obj.Value.(*model.AllocationBlock)} err = block.assign(args.IP, args.HandleID, args.Attrs, hostname) if err != nil { log.Errorf("Failed to assign address %v: %v", args.IP, err) return err } // Increment handle. if args.HandleID != nil { c.incrementHandle(ctx, *args.HandleID, blockCIDR, 1) } // Update the block using the original KVPair to do a CAS. No need to // update the Value since we have been manipulating the Value pointed to // in the KVPair. _, err = c.blockReaderWriter.updateBlock(ctx, obj) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { log.WithError(err).Debug("CAS error assigning IP - retry") continue } log.WithError(err).Warningf("Update failed on block %s", block.CIDR.String()) if args.HandleID != nil { if err := c.decrementHandle(ctx, *args.HandleID, blockCIDR, 1); err != nil { log.WithError(err).Warn("Failed to decrement handle") } } return err } return nil } return errors.New("Max retries hit - excessive concurrent IPAM requests") } // ReleaseIPs releases any of the given IP addresses that are currently assigned, // so that they are available to be used in another assignment. func (c ipamClient) ReleaseIPs(ctx context.Context, ips []net.IP) ([]net.IP, error) { log.Infof("Releasing IP addresses: %v", ips) unallocated := []net.IP{} // Group IP addresses by block to minimize the number of writes // to the datastore required to release the given addresses. ipsByBlock := map[string][]net.IP{} for _, ip := range ips { var cidrStr string pool, err := c.blockReaderWriter.getPoolForIP(ip, nil) if err != nil { log.WithError(err).Warnf("Failed to get pool for IP") return nil, err } if pool == nil { if cidr, err := c.blockReaderWriter.getBlockForIP(ctx, ip); err != nil { return nil, err } else { if cidr == nil { // The IP isn't in any block so it's already unallocated. unallocated = append(unallocated, ip) // Move on to the next IP continue } cidrStr = cidr.String() } } else { cidrStr = getBlockCIDRForAddress(ip, pool).String() } // Check if we've already got an entry for this block. if _, exists := ipsByBlock[cidrStr]; !exists { // Entry does not exist, create it. ipsByBlock[cidrStr] = []net.IP{} } // Append to the list. ipsByBlock[cidrStr] = append(ipsByBlock[cidrStr], ip) } // Release IPs for each block. for cidrStr, ips := range ipsByBlock { _, cidr, _ := net.ParseCIDR(cidrStr) unalloc, err := c.releaseIPsFromBlock(ctx, ips, *cidr) if err != nil { log.Errorf("Error releasing IPs: %v", err) return nil, err } unallocated = append(unallocated, unalloc...) } return unallocated, nil } func (c ipamClient) releaseIPsFromBlock(ctx context.Context, ips []net.IP, blockCIDR net.IPNet) ([]net.IP, error) { logCtx := log.WithField("cidr", blockCIDR) for i := 0; i < datastoreRetries; i++ { logCtx.Info("Getting block so we can release IPs") // Get allocation block for cidr. obj, err := c.blockReaderWriter.queryBlock(ctx, blockCIDR, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // The block does not exist - all addresses must be unassigned. return ips, nil } else { // Unexpected error reading block. return nil, err } } // Release the IPs. b := allocationBlock{obj.Value.(*model.AllocationBlock)} unallocated, handles, err2 := b.release(ips) if err2 != nil { return nil, err2 } if len(ips) == len(unallocated) { // All the given IP addresses are already unallocated. // Just return. logCtx.Info("No IPs need to be released") return unallocated, nil } // If the block is empty and has no affinity, we can delete it. // Otherwise, update the block using CAS. There is no need to update // the Value since we have updated the structure pointed to in the // KVPair. var updateErr error if b.empty() && b.Affinity == nil { logCtx.Info("Deleting non-affine block") updateErr = c.blockReaderWriter.deleteBlock(ctx, obj) } else { logCtx.Info("Updating assignments in block") _, updateErr = c.blockReaderWriter.updateBlock(ctx, obj) } if updateErr != nil { if _, ok := updateErr.(cerrors.ErrorResourceUpdateConflict); ok { // Comparison error - retry. logCtx.Warningf("Failed to update block - retry #%d", i) continue } else { // Something else - return the error. logCtx.WithError(updateErr).Errorf("Error updating block") return nil, updateErr } } // Success - decrement handles. logCtx.Debugf("Decrementing handles: %v", handles) for handleID, amount := range handles { if err := c.decrementHandle(ctx, handleID, blockCIDR, amount); err != nil { logCtx.WithError(err).Warn("Failed to decrement handle") } } // Determine whether or not the block's pool still matches the node. if err := c.ensureConsistentAffinity(ctx, obj.Value.(*model.AllocationBlock)); err != nil { logCtx.WithError(err).Warn("Error ensuring consistent affinity but IP already released. Returning no error.") } return unallocated, nil } return nil, errors.New("Max retries hit - excessive concurrent IPAM requests") } func (c ipamClient) assignFromExistingBlock(ctx context.Context, block *model.KVPair, num int, handleID *string, attrs map[string]string, host string, affCheck bool) ([]net.IPNet, error) { blockCIDR := block.Key.(model.BlockKey).CIDR logCtx := log.WithFields(log.Fields{"host": host, "block": blockCIDR}) if handleID != nil { logCtx = logCtx.WithField("handle", *handleID) } logCtx.Infof("Attempting to assign %d addresses from block", num) // Pull out the block. b := allocationBlock{block.Value.(*model.AllocationBlock)} ips, err := b.autoAssign(num, handleID, host, attrs, affCheck) if err != nil { logCtx.WithError(err).Errorf("Error in auto assign") return nil, err } if len(ips) == 0 { logCtx.Infof("Block is full") return []net.IPNet{}, nil } // Increment handle count. if handleID != nil { logCtx.Debug("Incrementing handle") c.incrementHandle(ctx, *handleID, blockCIDR, num) } // Update the block using CAS by passing back the original // KVPair. logCtx.Info("Writing block in order to claim IPs") block.Value = b.AllocationBlock _, err = c.blockReaderWriter.updateBlock(ctx, block) if err != nil { logCtx.WithError(err).Infof("Failed to update block") if handleID != nil { logCtx.Debug("Decrementing handle since we failed to allocate IP(s)") if err := c.decrementHandle(ctx, *handleID, blockCIDR, num); err != nil { logCtx.WithError(err).Warnf("Failed to decrement handle") } } return nil, err } logCtx.Infof("Successfully claimed IPs: %v", ips) return ips, nil } // ClaimAffinity makes a best effort to claim affinity to the given host for all blocks // within the given CIDR. The given CIDR must fall within a configured // pool. Returns a list of blocks that were claimed, as well as a // list of blocks that were claimed by another host. // If an empty string is passed as the host, then the hostname is automatically detected. func (c ipamClient) ClaimAffinity(ctx context.Context, cidr net.IPNet, host string) ([]net.IPNet, []net.IPNet, error) { logCtx := log.WithFields(log.Fields{"host": host, "cidr": cidr}) // Verify the requested CIDR falls within a configured pool. pool, err := c.blockReaderWriter.getPoolForIP(net.IP{IP: cidr.IP}, nil) if err != nil { return nil, nil, err } if pool == nil { estr := fmt.Sprintf("The requested CIDR (%s) is not within any configured pools.", cidr.String()) return nil, nil, errors.New(estr) } // Validate that the given CIDR is at least as big as a block. if !largerThanOrEqualToBlock(cidr, pool) { estr := fmt.Sprintf("The requested CIDR (%s) is smaller than the minimum.", cidr.String()) return nil, nil, invalidSizeError(estr) } // Determine the hostname to use. hostname, err := decideHostname(host) if err != nil { return nil, nil, err } failed := []net.IPNet{} claimed := []net.IPNet{} // Get IPAM config. cfg, err := c.GetIPAMConfig(ctx) if err != nil { logCtx.Errorf("Failed to get IPAM Config: %v", err) return nil, nil, err } // Claim all blocks within the given cidr. blocks := blockGenerator(pool, cidr) for blockCIDR := blocks(); blockCIDR != nil; blockCIDR = blocks() { for i := 0; i < datastoreRetries; i++ { // First, claim a pending affinity. pa, err := c.blockReaderWriter.getPendingAffinity(ctx, hostname, *blockCIDR) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error getting pending affinity - retry") continue } return claimed, failed, err } // Once we have the affinity, claim the block, which will confirm the affinity. _, err = c.blockReaderWriter.claimAffineBlock(ctx, pa, *cfg) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error claiming affine block - retry") continue } else if _, ok := err.(errBlockClaimConflict); ok { logCtx.Debugf("Block %s is claimed by another host", blockCIDR.String()) failed = append(failed, *blockCIDR) } else { logCtx.Errorf("Failed to claim block: %v", err) return claimed, failed, err } } else { logCtx.Debugf("Claimed CIDR %s", blockCIDR.String()) claimed = append(claimed, *blockCIDR) } break } } return claimed, failed, nil } // ReleaseAffinity releases affinity for all blocks within the given CIDR // on the given host. If a block does not have affinity for the given host, // its affinity will not be released and no error will be returned. // If an empty string is passed as the host, then the hostname is automatically detected. func (c ipamClient) ReleaseAffinity(ctx context.Context, cidr net.IPNet, host string, mustBeEmpty bool) error { // Verify the requested CIDR falls within a configured pool. fields := log.Fields{"cidr": cidr.String(), "host": host, "mustBeEmpty": mustBeEmpty} log.WithFields(fields).Debugf("Releasing affinity for CIDR") pool, err := c.blockReaderWriter.getPoolForIP(net.IP{IP: cidr.IP}, nil) if pool == nil { estr := fmt.Sprintf("The requested CIDR (%s) is not within any configured pools.", cidr.String()) return errors.New(estr) } // Validate that the given CIDR is at least as big as a block. if !largerThanOrEqualToBlock(cidr, pool) { estr := fmt.Sprintf("The requested CIDR (%s) is smaller than the minimum.", cidr.String()) return invalidSizeError(estr) } // Determine the hostname to use. hostname, err := decideHostname(host) if err != nil { return err } // Release all blocks within the given cidr. blocks := blockGenerator(pool, cidr) for blockCIDR := blocks(); blockCIDR != nil; blockCIDR = blocks() { logCtx := log.WithField("cidr", blockCIDR) for i := 0; i < datastoreRetries; i++ { err := c.blockReaderWriter.releaseBlockAffinity(ctx, hostname, *blockCIDR, mustBeEmpty) if err != nil { if _, ok := err.(errBlockClaimConflict); ok { // Not claimed by this host - ignore. } else if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // Block does not exist - ignore. } else if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error releasing block affinity - retry") continue } else { logCtx.WithError(err).Errorf("Error releasing affinity") return err } } break } } return nil } // ReleaseHostAffinities releases affinity for all blocks that are affine // to the given host. If an empty string is passed as the host, // then the hostname is automatically detected. func (c ipamClient) ReleaseHostAffinities(ctx context.Context, host string, mustBeEmpty bool) error { log.Debugf("Releasing affinities for host %s. MustBeEmpty? %v", host, mustBeEmpty) hostname, err := decideHostname(host) if err != nil { return err } versions := []int{4, 6} for _, version := range versions { blockCIDRs, _, err := c.blockReaderWriter.getAffineBlocks(ctx, hostname, version, nil) if err != nil { return err } for _, blockCIDR := range blockCIDRs { err := c.ReleaseAffinity(ctx, blockCIDR, hostname, mustBeEmpty) if err != nil { if _, ok := err.(errBlockClaimConflict); ok { // Claimed by a different host. } else { return err } } } } return nil } // ReleasePoolAffinities releases affinity for all blocks within // the specified pool across all hosts. func (c ipamClient) ReleasePoolAffinities(ctx context.Context, pool net.IPNet) error { log.Infof("Releasing block affinities within pool '%s'", pool.String()) for i := 0; i < ipamKeyErrRetries; i++ { retry := false pairs, err := c.hostBlockPairs(ctx, pool) if err != nil { return err } if len(pairs) == 0 { log.Debugf("No blocks have affinity") return nil } for blockString, host := range pairs { _, blockCIDR, _ := net.ParseCIDR(blockString) logCtx := log.WithField("cidr", blockCIDR) for i := 0; i < datastoreRetries; i++ { err = c.blockReaderWriter.releaseBlockAffinity(ctx, host, *blockCIDR, false) if err != nil { if _, ok := err.(errBlockClaimConflict); ok { retry = true } else if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { logCtx.Debugf("No such block") break } else if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.WithError(err).Debug("CAS error releasing block affinity - retry") continue } else { logCtx.WithError(err).Errorf("Error releasing affinity") return err } } break } } if !retry { return nil } } return errors.New("Max retries hit - excessive concurrent IPAM requests") } // RemoveIPAMHost releases affinity for all blocks on the given host, // and removes all host-specific IPAM data from the datastore. // RemoveIPAMHost does not release any IP addresses claimed on the given host. // If an empty string is passed as the host, then the hostname is automatically detected. func (c ipamClient) RemoveIPAMHost(ctx context.Context, host string) error { // Determine the hostname to use. hostname, err := decideHostname(host) if err != nil { return err } logCtx := log.WithField("host", hostname) logCtx.Info("Removing IPAM data for host") for i := 0; i < datastoreRetries; i++ { // Release affinities for this host. logCtx.Info("Releasing IPAM affinities for host") if err := c.ReleaseHostAffinities(ctx, hostname, false); err != nil { logCtx.WithError(err).Errorf("Failed to release IPAM affinities for host") return err } // Get the IPAM host. logCtx.Info("Querying IPAM host tree in data store") k := model.IPAMHostKey{Host: hostname} kvp, err := c.client.Get(ctx, k, "") if err != nil { if _, ok := err.(cerrors.ErrorOperationNotSupported); ok { // KDD mode doesn't have this object - this is a no-op. logCtx.Debugf("No need to remove IPAM host for this datastore") return nil } if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok { logCtx.WithError(err).Errorf("Failed to get IPAM host") return err } // Resource does not exist, no need to remove it. logCtx.Info("IPAM host data does not exist") return nil } // Remove the host tree from the datastore. logCtx.Info("Deleting IPAM host tree from data store") _, err = c.client.Delete(ctx, k, kvp.Revision) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { // We hit a compare-and-delete error - retry. continue } // Return the error unless the resource does not exist. if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok { logCtx.Errorf("Error removing IPAM host: %v", err) return err } } logCtx.Info("Successfully deleted IPAM host data") return nil } return errors.New("Max retries hit") } func (c ipamClient) hostBlockPairs(ctx context.Context, pool net.IPNet) (map[string]string, error) { pairs := map[string]string{} // Get all blocks and their affinities. objs, err := c.client.List(ctx, model.BlockAffinityListOptions{}, "") if err != nil { log.Errorf("Error querying block affinities: %v", err) return nil, err } // Iterate through each block affinity and build up a mapping // of blockCidr -> host. log.Debugf("Getting block -> host mappings") for _, o := range objs.KVPairs { k := o.Key.(model.BlockAffinityKey) // Only add the pair to the map if the block belongs to the pool. if pool.Contains(k.CIDR.IPNet.IP) { pairs[k.CIDR.String()] = k.Host } log.Debugf("Block %s -> %s", k.CIDR.String(), k.Host) } return pairs, nil } // IpsByHandle returns a list of all IP addresses that have been // assigned using the provided handle. func (c ipamClient) IPsByHandle(ctx context.Context, handleID string) ([]net.IP, error) { obj, err := c.blockReaderWriter.queryHandle(ctx, handleID, "") if err != nil { return nil, err } handle := allocationHandle{obj.Value.(*model.IPAMHandle)} assignments := []net.IP{} for k, _ := range handle.Block { _, blockCIDR, _ := net.ParseCIDR(k) obj, err := c.blockReaderWriter.queryBlock(ctx, *blockCIDR, "") if err != nil { log.WithError(err).Warningf("Couldn't read block %s referenced by handle %s", blockCIDR, handleID) continue } // Pull out the allocationBlock and get all the assignments from it. b := allocationBlock{obj.Value.(*model.AllocationBlock)} assignments = append(assignments, b.ipsByHandle(handleID)...) } return assignments, nil } // ReleaseByHandle releases all IP addresses that have been assigned // using the provided handle. func (c ipamClient) ReleaseByHandle(ctx context.Context, handleID string) error { log.Infof("Releasing all IPs with handle '%s'", handleID) obj, err := c.blockReaderWriter.queryHandle(ctx, handleID, "") if err != nil { return err } handle := allocationHandle{obj.Value.(*model.IPAMHandle)} for blockStr, _ := range handle.Block { _, blockCIDR, _ := net.ParseCIDR(blockStr) if err := c.releaseByHandle(ctx, handleID, *blockCIDR); err != nil { return err } } return nil } func (c ipamClient) releaseByHandle(ctx context.Context, handleID string, blockCIDR net.IPNet) error { logCtx := log.WithFields(log.Fields{"handle": handleID, "cidr": blockCIDR}) for i := 0; i < datastoreRetries; i++ { logCtx.Debug("Querying block so we can release IPs by handle") obj, err := c.blockReaderWriter.queryBlock(ctx, blockCIDR, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // Block doesn't exist, so all addresses are already // unallocated. This can happen when a handle is // overestimating the number of assigned addresses. return nil } else { return err } } // Release the IP by handle. block := allocationBlock{obj.Value.(*model.AllocationBlock)} num := block.releaseByHandle(handleID) if num == 0 { // Block has no addresses with this handle, so // all addresses are already unallocated. logCtx.Debug("Block has no addresses with the given handle") return nil } logCtx.Debugf("Block has %d IPs with the given handle", num) if block.empty() && block.Affinity == nil { logCtx.Info("Deleting block because it is now empty and has no affinity") err = c.blockReaderWriter.deleteBlock(ctx, obj) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { logCtx.Debug("CAD error deleting block - retry") continue } // Return the error unless the resource does not exist. if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok { logCtx.Errorf("Error deleting block: %v", err) return err } } logCtx.Info("Successfully deleted empty block") } else { // Compare and swap the AllocationBlock using the original // KVPair read from before. No need to update the Value since we // have been directly manipulating the value referenced by the KVPair. logCtx.Debug("Updating block to release IPs") _, err = c.blockReaderWriter.updateBlock(ctx, obj) if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { // Comparison failed - retry. logCtx.Warningf("CAS error for block, retry #%d: %v", i, err) continue } else { // Something else - return the error. logCtx.Errorf("Error updating block '%s': %v", block.CIDR.String(), err) return err } } logCtx.Debug("Successfully released IPs from block") } if err = c.decrementHandle(ctx, handleID, blockCIDR, num); err != nil { logCtx.WithError(err).Warn("Failed to decrement handle") } // Determine whether or not the block's pool still matches the node. if err = c.ensureConsistentAffinity(ctx, block.AllocationBlock); err != nil { logCtx.WithError(err).Warn("Error ensuring consistent affinity but IP already released. Returning no error.") } return nil } return errors.New("Hit max retries") } func (c ipamClient) incrementHandle(ctx context.Context, handleID string, blockCIDR net.IPNet, num int) error { var obj *model.KVPair var err error for i := 0; i < datastoreRetries; i++ { obj, err = c.blockReaderWriter.queryHandle(ctx, handleID, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // Handle doesn't exist - create it. log.Infof("Creating new handle: %s", handleID) bh := model.IPAMHandle{ HandleID: handleID, Block: map[string]int{}, } obj = &model.KVPair{ Key: model.IPAMHandleKey{HandleID: handleID}, Value: &bh, } } else { // Unexpected error reading handle. return err } } // Get the handle from the KVPair. handle := allocationHandle{obj.Value.(*model.IPAMHandle)} // Increment the handle for this block. handle.incrementBlock(blockCIDR, num) // Compare and swap the handle using the KVPair from above. We've been // manipulating the structure in the KVPair, so pass straight back to // apply the changes. if obj.Revision != "" { // This is an existing handle - update it. _, err = c.blockReaderWriter.updateHandle(ctx, obj) if err != nil { log.WithError(err).Warning("Failed to update handle, retry") continue } } else { // This is a new handle - create it. _, err = c.client.Create(ctx, obj) if err != nil { log.WithError(err).Warning("Failed to create handle, retry") continue } } return nil } return errors.New("Max retries hit - excessive concurrent IPAM requests") } func (c ipamClient) decrementHandle(ctx context.Context, handleID string, blockCIDR net.IPNet, num int) error { for i := 0; i < datastoreRetries; i++ { obj, err := c.blockReaderWriter.queryHandle(ctx, handleID, "") if err != nil { return err } handle := allocationHandle{obj.Value.(*model.IPAMHandle)} _, err = handle.decrementBlock(blockCIDR, num) if err != nil { return err } // Update / Delete as appropriate. Since we have been manipulating the // data in the KVPair, just pass this straight back to the client. if handle.empty() { log.Debugf("Deleting handle: %s", handleID) if err = c.blockReaderWriter.deleteHandle(ctx, obj); err != nil { if err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { // Update conflict - retry. continue } else if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok { return err } // Already deleted. } } } else { log.Debugf("Updating handle: %s", handleID) if _, err = c.blockReaderWriter.updateHandle(ctx, obj); err != nil { if _, ok := err.(cerrors.ErrorResourceUpdateConflict); ok { // Update conflict - retry. continue } return err } } log.Debugf("Decremented handle '%s' by %d", handleID, num) return nil } return errors.New("Max retries hit - excessive concurrent IPAM requests") } // GetAssignmentAttributes returns the attributes stored with the given IP address // upon assignment. func (c ipamClient) GetAssignmentAttributes(ctx context.Context, addr net.IP) (map[string]string, error) { pool, err := c.blockReaderWriter.getPoolForIP(addr, nil) if err != nil { return nil, err } if pool == nil { log.Errorf("Error reading pool for %s", addr.String()) return nil, errors.New(fmt.Sprintf("%s is not part of a configured pool", addr)) } blockCIDR := getBlockCIDRForAddress(addr, pool) obj, err := c.blockReaderWriter.queryBlock(ctx, blockCIDR, "") if err != nil { log.Errorf("Error reading block %s: %v", blockCIDR, err) return nil, errors.New(fmt.Sprintf("%s is not assigned", addr)) } block := allocationBlock{obj.Value.(*model.AllocationBlock)} return block.attributesForIP(addr) } // GetIPAMConfig returns the global IPAM configuration. If no IPAM configuration // has been set, returns a default configuration with StrictAffinity disabled // and AutoAllocateBlocks enabled. func (c ipamClient) GetIPAMConfig(ctx context.Context) (*IPAMConfig, error) { obj, err := c.client.Get(ctx, model.IPAMConfigKey{}, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // IPAMConfig has not been explicitly set. Return // a default IPAM configuration. return &IPAMConfig{AutoAllocateBlocks: true, StrictAffinity: false}, nil } log.Errorf("Error getting IPAMConfig: %v", err) return nil, err } return c.convertBackendToIPAMConfig(obj.Value.(*model.IPAMConfig)), nil } // SetIPAMConfig sets global IPAM configuration. This can only // be done when there are no allocated blocks and IP addresses. func (c ipamClient) SetIPAMConfig(ctx context.Context, cfg IPAMConfig) error { current, err := c.GetIPAMConfig(ctx) if err != nil { return err } if *current == cfg { return nil } if !cfg.StrictAffinity && !cfg.AutoAllocateBlocks { return errors.New("Cannot disable 'StrictAffinity' and 'AutoAllocateBlocks' at the same time") } allObjs, err := c.client.List(ctx, model.BlockListOptions{}, "") if len(allObjs.KVPairs) != 0 { return errors.New("Cannot change IPAM config while allocations exist") } // Write to datastore. obj := model.KVPair{ Key: model.IPAMConfigKey{}, Value: c.convertIPAMConfigToBackend(&cfg), } _, err = c.client.Apply(ctx, &obj) if err != nil { log.Errorf("Error applying IPAMConfig: %v", err) return err } return nil } func (c ipamClient) convertIPAMConfigToBackend(cfg *IPAMConfig) *model.IPAMConfig { return &model.IPAMConfig{ StrictAffinity: cfg.StrictAffinity, AutoAllocateBlocks: cfg.AutoAllocateBlocks, } } func (c ipamClient) convertBackendToIPAMConfig(cfg *model.IPAMConfig) *IPAMConfig { return &IPAMConfig{ StrictAffinity: cfg.StrictAffinity, AutoAllocateBlocks: cfg.AutoAllocateBlocks, } } // ensureConsistentAffinity retrieves the pool and node for the given block and determines // if the pool still selects node. If it no longer matches, it will release the block // affinity for that node. // Returns a bool indicating if the block affinity was released. func (c ipamClient) ensureConsistentAffinity(ctx context.Context, b *model.AllocationBlock) error { // Retrieve node for this allocation. We do this so we can clean up affinity for blocks // which should no longer be affine to this host. host := getHostAffinity(b) logCtx := log.WithFields(log.Fields{"cidr": b.CIDR, "host": host}) // If no hostname is found on the block affinity, // there is no need to do an ip pool node selection check. if host == "" { logCtx.Debug("Block already has no affinity") return nil } // If the IP pool which owns this block no longer selects this node, // we should release the block's affinity to this node so it can be // used elsewhere. logCtx.Debugf("Looking up node labels for host affinity") node, err := c.client.Get(ctx, model.ResourceKey{Kind: v3.KindNode, Name: host}, "") if err != nil { if _, ok := err.(cerrors.ErrorResourceDoesNotExist); !ok { logCtx.WithError(err).WithField("node", host).Error("Failed to get node for host") return err } logCtx.Info("Node doesn't exist, no need to release affinity") return nil } // Make sure the returned value is a valid node. v3n, ok := node.Value.(*v3.Node) if !ok { return fmt.Errorf("Datastore returned malformed node object") } // Fetch the pool for the given CIDR and check if it selects the node. pool, err := c.blockReaderWriter.getPoolForIP(net.IP{b.CIDR.IPNet.IP}, nil) if err != nil { return err } if pool == nil { logCtx.Debug("No pools own this block") return nil } else if sel, err := pool.SelectsNode(*v3n); err != nil { logCtx.WithField("selector", pool.Spec.NodeSelector).WithError(err).Error("Failed to determine node selection") return err } else if sel { logCtx.Debug("Pool selects node, no change") return nil } logCtx.WithField("selector", pool.Spec.NodeSelector).Debug("Pool no longer selects node, releasing block affinity") // Pool does not match this node's label, release this block's affinity. if err = c.blockReaderWriter.releaseBlockAffinity(ctx, host, b.CIDR, true); err != nil { if _, ok := err.(errBlockClaimConflict); ok { // Not claimed by this host - ignore. } else if _, ok := err.(errBlockNotEmpty); ok { // Block isn't empty - ignore. } else if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok { // Block does not exist - ignore. } else { return err } } return nil } func decideHostname(host string) (string, error) { // Determine the hostname to use - prefer the provided hostname if // non-nil, otherwise use the hostname reported by os. var hostname string var err error if host != "" { hostname = host } else { hostname, err = names.Hostname() if err != nil { return "", fmt.Errorf("Failed to acquire hostname: %+v", err) } } log.Debugf("Using hostname=%s", hostname) return hostname, nil } // GetUtilization returns IP utilization info for the specified pools, or for all pools. func (c ipamClient) GetUtilization(ctx context.Context, args GetUtilizationArgs) ([]*PoolUtilization, error) { var usage []*PoolUtilization // Read all pools. allPools, err := c.pools.GetAllPools() if err != nil { log.WithError(err).Errorf("Error getting IP pools") return nil, err } // Identify the ones we want and create a PoolUtilization for each of those. wantAllPools := len(args.Pools) == 0 wantedPools := set.FromArray(args.Pools) for _, pool := range allPools { if wantAllPools || wantedPools.Contains(pool.Name) || wantedPools.Contains(pool.Spec.CIDR) { usage = append(usage, &PoolUtilization{ Name: pool.Name, CIDR: net.MustParseNetwork(pool.Spec.CIDR).IPNet, }) } } // If we've been asked for all pools, also report utilization for any allocation // blocks for which there is no longer an IP pool. Note: following code depends // on this being at the end of the list; otherwise it will suck in allocation // blocks that should be reported under other pools. if wantAllPools { usage = append(usage, &PoolUtilization{ Name: "orphaned allocation blocks", CIDR: net.MustParseNetwork("0.0.0.0/0").IPNet, }) } // Read all allocation blocks. blocks, err := c.client.List(ctx, model.BlockListOptions{}, "") if err != nil { return nil, err } for _, kvp := range blocks.KVPairs { b := kvp.Value.(*model.AllocationBlock) log.Debugf("Got block: %v", b) // Find which pool this block belongs to. for _, poolUse := range usage { if b.CIDR.IsNetOverlap(poolUse.CIDR) { log.Debugf("Block CIDR %v belongs to pool %v", b.CIDR, poolUse.Name) poolUse.Blocks = append(poolUse.Blocks, BlockUtilization{ CIDR: b.CIDR.IPNet, Capacity: b.NumAddresses(), Available: len(b.Unallocated), }) break } } } return usage, nil }