Skip to content

Commit

Permalink
Merge pull request #1087 from neiljerram/ipam-utilization
Browse files Browse the repository at this point in the history
IPAM utilization reporting
  • Loading branch information
Neil Jerram committed May 29, 2019
2 parents 5015d7d + 7242457 commit 89ae6d0
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 44 deletions.
5 changes: 3 additions & 2 deletions lib/backend/model/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,10 @@ import (
"regexp"
"strings"

log "github.com/sirupsen/logrus"

"github.com/projectcalico/libcalico-go/lib/errors"
"github.com/projectcalico/libcalico-go/lib/net"
log "github.com/sirupsen/logrus"
)

const (
Expand Down Expand Up @@ -104,7 +105,7 @@ type AllocationBlock struct {
Deleted bool `json:"deleted"`

// HostAffinity is deprecated in favor of Affinity.
// This is only to keep compatiblity with existing deployments.
// This is only to keep compatibility with existing deployments.
// The data format should be `Affinity: host:hostname` (not `hostAffinity: hostname`).
HostAffinity *string `json:"hostAffinity,omitempty"`
}
Expand Down
43 changes: 30 additions & 13 deletions lib/clientv3/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"strings"
"time"

"github.com/satori/go.uuid"

"github.com/projectcalico/libcalico-go/lib/apiconfig"
"github.com/projectcalico/libcalico-go/lib/apis/v3"
"github.com/projectcalico/libcalico-go/lib/backend"
Expand All @@ -33,7 +35,6 @@ import (
"github.com/projectcalico/libcalico-go/lib/net"
"github.com/projectcalico/libcalico-go/lib/options"
"github.com/projectcalico/libcalico-go/lib/set"
"github.com/satori/go.uuid"
)

// client implements the client.Interface.
Expand Down Expand Up @@ -148,26 +149,42 @@ type poolAccessor struct {
}

func (p poolAccessor) GetEnabledPools(ipVersion int) ([]v3.IPPool, error) {
pools, err := p.client.IPPools().List(context.Background(), options.ListOptions{})
if err != nil {
return nil, err
}
log.Debugf("Got list of all IPPools: %v", pools)
var enabled []v3.IPPool
for _, pool := range pools.Items {
return p.getPools(func(pool *v3.IPPool) bool {
if pool.Spec.Disabled {
log.Debugf("Skipping disabled IP pool (%s)", pool.Name)
continue
} else if _, cidr, err := net.ParseCIDR(pool.Spec.CIDR); err == nil && cidr.Version() == ipVersion {
log.Debugf("Adding pool (%s) to the enabled IPPool list", cidr.String())
enabled = append(enabled, pool)
return false
}
if _, cidr, err := net.ParseCIDR(pool.Spec.CIDR); err == nil && cidr.Version() == ipVersion {
log.Debugf("Adding pool (%s) to the IPPool list", cidr.String())
return true
} else if err != nil {
log.Warnf("Failed to parse the IPPool: %s. Ignoring that IPPool", pool.Spec.CIDR)
} else {
log.Debugf("Ignoring IPPool: %s. IP version is different.", pool.Spec.CIDR)
}
return false
})
}

func (p poolAccessor) getPools(filter func(pool *v3.IPPool) bool) ([]v3.IPPool, error) {
pools, err := p.client.IPPools().List(context.Background(), options.ListOptions{})
if err != nil {
return nil, err
}
log.Debugf("Got list of all IPPools: %v", pools)
var filtered []v3.IPPool
for _, pool := range pools.Items {
if filter(&pool) {
filtered = append(filtered, pool)
}
}
return enabled, nil
return filtered, nil
}

func (p poolAccessor) GetAllPools() ([]v3.IPPool, error) {
return p.getPools(func(pool *v3.IPPool) bool {
return true
})
}

// EnsureInitialized is used to ensure the backend datastore is correctly
Expand Down
34 changes: 16 additions & 18 deletions lib/clientv3/ippool.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2017-2018 Tigera, Inc. All rights reserved.
// Copyright (c) 2017-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.
Expand All @@ -20,14 +20,15 @@ import (
"net"
"time"

log "github.com/sirupsen/logrus"

apiv3 "github.com/projectcalico/libcalico-go/lib/apis/v3"
"github.com/projectcalico/libcalico-go/lib/backend/model"
cerrors "github.com/projectcalico/libcalico-go/lib/errors"
cnet "github.com/projectcalico/libcalico-go/lib/net"
"github.com/projectcalico/libcalico-go/lib/options"
validator "github.com/projectcalico/libcalico-go/lib/validator/v3"
"github.com/projectcalico/libcalico-go/lib/watch"
log "github.com/sirupsen/logrus"
)

// IPPoolInterface has methods to work with IPPool resources.
Expand Down Expand Up @@ -80,23 +81,20 @@ func (r ipPools) Create(ctx context.Context, res *apiv3.IPPool, opts options.Set
}

blocks, err := r.client.backend.List(ctx, model.BlockListOptions{IPVersion: ipVersion}, "")
if _, ok := err.(cerrors.ErrorOperationNotSupported); !ok && err != nil {
// There was an error and it wasn't OperationNotSupported - return it.
if err != nil {
return nil, err
} else if err == nil {
// Skip the block check if the error is OperationUnsupported - IPAM is not supported on KDD.
for _, b := range blocks.KVPairs {
k := b.Key.(model.BlockKey)
ones, _ := k.CIDR.Mask.Size()
// Check if this block has a different size to the pool, and that it overlaps with the pool.
if ones != poolBlockSize && k.CIDR.IsNetOverlap(*poolCIDR) {
return nil, cerrors.ErrorValidation{
ErroredFields: []cerrors.ErroredField{{
Name: "IPPool.Spec.BlockSize",
Reason: "IPPool blocksSize conflicts with existing allocations that use a different blockSize",
Value: res.Spec.BlockSize,
}},
}
}
for _, b := range blocks.KVPairs {
k := b.Key.(model.BlockKey)
ones, _ := k.CIDR.Mask.Size()
// Check if this block has a different size to the pool, and that it overlaps with the pool.
if ones != poolBlockSize && k.CIDR.IsNetOverlap(*poolCIDR) {
return nil, cerrors.ErrorValidation{
ErroredFields: []cerrors.ErroredField{{
Name: "IPPool.Spec.BlockSize",
Reason: "IPPool blocksSize conflicts with existing allocations that use a different blockSize",
Value: res.Spec.BlockSize,
}},
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion lib/ipam/interface.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2017-2018 Tigera, Inc. All rights reserved.
// Copyright (c) 2017-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.
Expand Down Expand Up @@ -90,4 +90,7 @@ type Interface interface {
// RemoveIPAMHost does not release any IP addresses claimed on the given host.
// If an empty string is passed as the host then the value returned by os.Hostname is used.
RemoveIPAMHost(ctx context.Context, host string) error

// GetUtilization returns IP utilization info for the specified pools, or for all pools.
GetUtilization(ctx context.Context, args GetUtilizationArgs) ([]*PoolUtilization, error)
}
65 changes: 64 additions & 1 deletion lib/ipam/ipam.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,11 @@ import (
"errors"
"fmt"

"github.com/projectcalico/libcalico-go/lib/apis/v3"
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"
Expand Down Expand Up @@ -1524,3 +1526,64 @@ func decideHostname(host string) (string, error) {
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
}
7 changes: 3 additions & 4 deletions lib/ipam/ipam_block_reader_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@ import (
"net"
"time"

"github.com/projectcalico/libcalico-go/lib/apis/v3"
log "github.com/sirupsen/logrus"

"github.com/projectcalico/libcalico-go/lib/apis/v3"

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"
Expand All @@ -42,16 +43,14 @@ func (rw blockReaderWriter) getAffineBlocks(ctx context.Context, host string, ve
blocksInPool = []cnet.IPNet{}
blocksNotInPool = []cnet.IPNet{}

// Lookup all blocks by providing an empty BlockListOptions
// to the List operation.
// Lookup blocks affine to the specified host.
opts := model.BlockAffinityListOptions{Host: host, IPVersion: ver}
datastoreObjs, err := rw.client.List(ctx, opts, "")
if err != nil {
if _, ok := err.(cerrors.ErrorResourceDoesNotExist); ok {
// The block path does not exist yet. This is OK - it means
// there are no affine blocks.
return

} else {
log.Errorf("Error getting affine blocks: %v", err)
return
Expand Down
51 changes: 48 additions & 3 deletions lib/ipam/ipam_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ func (i *ipPoolAccessor) GetEnabledPools(ipVersion int) ([]v3.IPPool, error) {
sorted = append(sorted, p)
}
}
return i.getPools(sorted, ipVersion, "GetEnabledPools"), nil
}

func (i *ipPoolAccessor) getPools(sorted []string, ipVersion int, caller string) []v3.IPPool {
sort.Strings(sorted)

// Convert to IPNets and sort out the correct IP versions. Sorting the results
Expand All @@ -73,7 +77,7 @@ func (i *ipPoolAccessor) GetEnabledPools(ipVersion int) ([]v3.IPPool, error) {
pools := make([]v3.IPPool, 0)
for _, p := range sorted {
c := cnet.MustParseCIDR(p)
if c.Version() == ipVersion {
if (ipVersion == 0) || (c.Version() == ipVersion) {
pool := v3.IPPool{Spec: v3.IPPoolSpec{CIDR: p, NodeSelector: i.pools[p].nodeSelector}}
if i.pools[p].blockSize == 0 {
if ipVersion == 4 {
Expand All @@ -89,9 +93,18 @@ func (i *ipPoolAccessor) GetEnabledPools(ipVersion int) ([]v3.IPPool, error) {
}
}

log.Infof("GetEnabledPools returns: %v", pools)
log.Infof("%v returns: %v", caller, pools)

return pools
}

return pools, nil
func (i *ipPoolAccessor) GetAllPools() ([]v3.IPPool, error) {
sorted := make([]string, 0)
// Get a sorted list of pool CIDR strings.
for p := range i.pools {
sorted = append(sorted, p)
}
return i.getPools(sorted, 0, "GetAllPools"), nil
}

var (
Expand Down Expand Up @@ -611,6 +624,18 @@ var _ = testutils.E2eDatastoreDescribe("IPAM tests", testutils.DatastoreAll, fun
pool2 := cnet.MustParseNetwork("20.0.0.0/24")
var block1, block2 cnet.IPNet

findInUse := func(usage []*PoolUtilization, cidr string, expectedInUse int) bool {
for _, poolUse := range usage {
for _, blockUse := range poolUse.Blocks {
if (blockUse.CIDR.String() == cidr) &&
(blockUse.Available == blockUse.Capacity-expectedInUse) {
return true
}
}
}
return false
}

It("should get an IP from pool1 when explicitly requesting from that pool", func() {
bc.Clean()
deleteAllPools()
Expand All @@ -637,6 +662,16 @@ var _ = testutils.E2eDatastoreDescribe("IPAM tests", testutils.DatastoreAll, fun

Expect(outErr).NotTo(HaveOccurred())
Expect(pool1.IPNet.Contains(v4[0].IP)).To(BeTrue())

usage, err := ic.GetUtilization(context.Background(), GetUtilizationArgs{})
Expect(err).NotTo(HaveOccurred())
Expect(findInUse(usage, "10.0.0.0/26", 1)).To(BeTrue())

usage, err = ic.GetUtilization(context.Background(), GetUtilizationArgs{
Pools: []string{"20.0.0.0/24"},
})
Expect(err).NotTo(HaveOccurred())
Expect(findInUse(usage, "10.0.0.0/26", 1)).To(BeFalse())
})

It("should get an IP from pool2 when explicitly requesting from that pool", func() {
Expand All @@ -657,6 +692,16 @@ var _ = testutils.E2eDatastoreDescribe("IPAM tests", testutils.DatastoreAll, fun

Expect(outErr).NotTo(HaveOccurred())
Expect(block2.IPNet.Contains(v4[0].IP)).To(BeTrue())

usage, err := ic.GetUtilization(context.Background(), GetUtilizationArgs{})
Expect(err).NotTo(HaveOccurred())
Expect(findInUse(usage, "20.0.0.0/26", 1)).To(BeTrue())

usage, err = ic.GetUtilization(context.Background(), GetUtilizationArgs{
Pools: []string{"20.0.0.0/24"},
})
Expect(err).NotTo(HaveOccurred())
Expect(findInUse(usage, "20.0.0.0/26", 1)).To(BeTrue())
})

It("should get an IP from pool1 in the same allocation block as the first IP from pool1", func() {
Expand Down
Loading

0 comments on commit 89ae6d0

Please sign in to comment.