From 2266868648488ab1334ebbf52d2ddc3599f2a3cf Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 13:27:00 +0100 Subject: [PATCH 01/10] Add the range.CIDRs functions --- net/address/address.go | 34 ++++++++++++++++++++++++++++++++++ net/address/address_test.go | 22 ++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/net/address/address.go b/net/address/address.go index 42aa3cc2..6252adcd 100644 --- a/net/address/address.go +++ b/net/address/address.go @@ -73,6 +73,40 @@ type CIDR struct { PrefixLen int } +// CIDRs returns a list of CIDR-aligned ranges which cover this range. +func (r Range) CIDRs() []CIDR { + const ( + fullMask = ^Address(0) + cidrMaxPrefixLen = 32 + ) + var cidrs []CIDR + + for start, end := r.Start, r.End-1; end >= start; { + mask, prefixLen := fullMask, cidrMaxPrefixLen + // Find the smallest mask which would cover some part of [start;end]. + // Once we found such, apply it by OR'ing + for mask > 0 { + tmpMask := mask << 1 + // Check whether mask neither too short nor too long + if (start&tmpMask) != start || (start|^tmpMask) > end { + break + } + mask = tmpMask + prefixLen-- + } + cidrs = append(cidrs, CIDR{start, prefixLen}) + // Apply mask + start |= ^mask + // Check for overflow + if start+1 < start { + break + } + start++ + } + + return cidrs +} + func ParseIP(s string) (Address, error) { if ip := net.ParseIP(s); ip != nil { return FromIP4(ip), nil diff --git a/net/address/address_test.go b/net/address/address_test.go index 46f699b4..326a5b2e 100644 --- a/net/address/address_test.go +++ b/net/address/address_test.go @@ -39,3 +39,25 @@ func TestBiggestPow2AlignedRange(t *testing.T) { } require.NoError(t, quick.Check(prop, &quick.Config{MaxCount: 1000000})) } + +func ip(s string) Address { + addr, _ := ParseIP(s) + return addr +} + +func cidr(s string) CIDR { + c, err := ParseCIDR(s) + if err != nil { + panic(err) + } + return c +} + +func TestCIDRs(t *testing.T) { + start := ip("10.0.0.1") + end := ip("10.0.0.9") + r := NewRange(start, Subtract(end, start)) + require.Equal(t, + []CIDR{cidr("10.0.0.1/32"), cidr("10.0.0.2/31"), cidr("10.0.0.4/30"), cidr("10.0.0.8/32")}, + r.CIDRs()) +} From 49f2e208c3181bfa854f0207b0c4a927f6f66d8c Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 13:37:36 +0100 Subject: [PATCH 02/10] Make ClaimForPeers to claim CIDR-aligned ranges --- ipam/ring/ring.go | 42 ++++++++++++++++++++++-------------------- ipam/ring/ring_test.go | 2 +- 2 files changed, 23 insertions(+), 21 deletions(-) diff --git a/ipam/ring/ring.go b/ipam/ring/ring.go index a9cf60dd..37df88f2 100644 --- a/ipam/ring/ring.go +++ b/ipam/ring/ring.go @@ -368,34 +368,36 @@ func (r *Ring) AllRangeInfo() (result []RangeInfo) { } // ClaimForPeers claims the entire ring for the array of peers passed -// in. Only works for empty rings. +// in. Only works for empty rings. Each claimed range is CIDR-aligned. func (r *Ring) ClaimForPeers(peers []mesh.PeerName) { common.Assert(r.Empty()) defer r.assertInvariants() defer r.updateExportedVariables() + defer func() { + e := r.Entries[len(r.Entries)-1] + common.Assert(address.Add(e.Token, address.Offset(e.Free)) == r.End) + }() - totalSize := r.distance(r.Start, r.End) - share := totalSize/address.Count(len(peers)) + 1 - remainder := totalSize % address.Count(len(peers)) - pos := r.Start - - for i, peer := range peers { - if address.Count(i) == remainder { - share-- - if share == 0 { - break - } - } - - r.Entries.insert(entry{Token: pos, Peer: peer, Free: share}) - pos += address.Address(share) - } - - common.Assert(pos == r.End) - + r.subdivide(r.Start, r.End, peers) r.Seeds = peers } +// subdivide subdivides the [from,to) CIDR for the given peers into +// CIDR-aligned subranges. +func (r *Ring) subdivide(from, to address.Address, peers []mesh.PeerName) { + share := address.Length(to, from) + if share == 0 { + return + } + if share == 1 || len(peers) == 1 { + r.Entries.insert(entry{Token: from, Peer: peers[0], Free: share}) + return + } + mid := address.Add(from, address.Offset(share/2)) + r.subdivide(from, mid, peers[:len(peers)/2]) + r.subdivide(address.Add(mid, address.Offset(share%2)), to, peers[len(peers)/2:]) +} + func (r *Ring) FprintWithNicknames(w io.Writer, m map[mesh.PeerName]string) { for _, entry := range r.Entries { nickname, found := m[entry.Peer] diff --git a/ipam/ring/ring_test.go b/ipam/ring/ring_test.go index 417c68b7..f360a088 100644 --- a/ipam/ring/ring_test.go +++ b/ipam/ring/ring_test.go @@ -540,7 +540,7 @@ func TestClaimForPeers(t *testing.T) { // Different end to usual so we get a number of addresses that a) // is smaller than the max number of peers, and b) is divisible by // some number of peers. This maximises coverage of edge cases. - end := dot10 + end := dot8 peers := makePeers(numPeers) // Test for a range of peer counts for i := 0; i < numPeers; i++ { From 59b3a3c8c57d87d4492821988940e20ef98281cf Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 13:39:11 +0100 Subject: [PATCH 03/10] Add the Tracker interface An instance of the interface must implement the HandleUpdate method which (in the future) will be called when any change to the IPAM ring occurs. --- ipam/tracker/tracker.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 ipam/tracker/tracker.go diff --git a/ipam/tracker/tracker.go b/ipam/tracker/tracker.go new file mode 100644 index 00000000..6a9846db --- /dev/null +++ b/ipam/tracker/tracker.go @@ -0,0 +1,20 @@ +package tracker + +import ( + "github.com/weaveworks/weave/net/address" +) + +// LocalRangeTracker is an interface for tracking changes in the IPAM ring. +type LocalRangeTracker interface { + // HandleUpdate is called whenever an address ring gets updated. + // + // prevRanges corresponds to ranges which were owned by a peer before + // a change in the ring, while currRanges to the ones which are currently + // owned by the peer. + // Both slices have to be sorted in increasing order. + // Adjacent ranges within each slice might appear as separate ranges. + // + // The local parameter indicates whether the ranges belong to the peer + // by which the method is called. + HandleUpdate(prevRanges, currRanges []address.Range, local bool) error +} From c260536429aa89f8ab362a95deb3ee4496ac395d Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 13:48:34 +0100 Subject: [PATCH 04/10] Add AWSVPC Tracker The tracker is responsible for managing the AWSVPC route tables which is used to implement the underlay routing based on VPC. --- .gitmodules | 3 + ipam/tracker/awsvpc.go | 286 +++++++++++++++++++++++++++++++ ipam/tracker/awsvpc_test.go | 52 ++++++ net/address/address.go | 16 ++ net/address/address_test.go | 6 + vendor/github.com/aws/aws-sdk-go | 1 + 6 files changed, 364 insertions(+) create mode 100644 ipam/tracker/awsvpc.go create mode 100644 ipam/tracker/awsvpc_test.go create mode 160000 vendor/github.com/aws/aws-sdk-go diff --git a/.gitmodules b/.gitmodules index 8636191c..b13e74dc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -73,3 +73,6 @@ [submodule "vendor/github.com/j-keck/arping"] path = vendor/github.com/j-keck/arping url = https://github.com/j-keck/arping +[submodule "vendor/github.com/aws/aws-sdk-go"] + path = vendor/github.com/aws/aws-sdk-go + url = https://github.com/aws/aws-sdk-go diff --git a/ipam/tracker/awsvpc.go b/ipam/tracker/awsvpc.go new file mode 100644 index 00000000..1089b615 --- /dev/null +++ b/ipam/tracker/awsvpc.go @@ -0,0 +1,286 @@ +package tracker + +// The AWSVPC tracker tracks the IPAM ring changes and accordingly updates +// the AWS VPC route table and, if the announced change is local +// (denoted by local=true) to the host, the host route table. +// +// During the initialization, the tracker detects AWS VPC route table id which +// is associated with the host's subnet on the AWS network. If such a table does +// not exist, the default VPC route table is used. +// +// When a host A donates a range to a host B, the necessary route table +// updates (removal) happen on the host A first, and afterwards on +// the host B (installation). +// +// NB: there is a hard limit for 50 routes within any VPC route table +// (practically, it is 48, because one route is used by the AWS Internet GW and +// one by the AWS host subnet), therefore it is suggested to avoid +// an excessive fragmentation within the IPAM ring which might happen due to +// the claim operations or uneven distribution of containers across the hosts. + +import ( + "fmt" + "net" + + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/ec2metadata" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/ec2" + "github.com/vishvananda/netlink" + + "github.com/weaveworks/weave/common" + wnet "github.com/weaveworks/weave/net" + "github.com/weaveworks/weave/net/address" +) + +type AWSVPCTracker struct { + ec2 *ec2.EC2 + instanceID string // EC2 Instance ID + routeTableID string // VPC Route Table ID + linkIndex int // The weave bridge link index +} + +// NewAWSVPCTracker creates and initialises AWS VPC based tracker. +func NewAWSVPCTracker() (*AWSVPCTracker, error) { + var ( + err error + session = session.New() + t = &AWSVPCTracker{} + ) + + // Detect region and instance id + meta := ec2metadata.New(session) + t.instanceID, err = meta.GetMetadata("instance-id") + if err != nil { + return nil, fmt.Errorf("cannot detect instance-id: %s", err) + } + region, err := meta.Region() + if err != nil { + return nil, fmt.Errorf("cannot detect region: %s", err) + } + + t.ec2 = ec2.New(session, aws.NewConfig().WithRegion(region)) + + routeTableID, err := t.detectRouteTableID() + if err != nil { + return nil, err + } + t.routeTableID = *routeTableID + + // Detect Weave bridge link index + link, err := netlink.LinkByName(wnet.WeaveBridgeName) + if err != nil { + return nil, fmt.Errorf("cannot find \"%s\" interface: %s", wnet.WeaveBridgeName, err) + } + t.linkIndex = link.Attrs().Index + + t.infof("AWSVPC has been initialized on %s instance for %s route table at %s region", + t.instanceID, t.routeTableID, region) + + return t, nil +} + +// HandleUpdate method updates the AWS VPC and the host route tables. +func (t *AWSVPCTracker) HandleUpdate(prevRanges, currRanges []address.Range, local bool) error { + t.debugf("replacing %q by %q; local(%t)", prevRanges, currRanges, local) + + prev, curr := removeCommon(address.NewCIDRs(merge(prevRanges)), address.NewCIDRs(merge(currRanges))) + + // It might make sense to do the removal first and then add entries + // because of the 50 routes limit. However, in such case a container might + // not be reachable for a short period of time which is not a desired behavior. + + // Add new entries + for _, cidr := range curr { + cidrStr := cidr.String() + t.debugf("adding route %s to %s", cidrStr, t.instanceID) + _, err := t.createVPCRoute(cidrStr) + if err != nil { + return fmt.Errorf("createVPCRoutes failed: %s", err) + } + if local { + err = t.createHostRoute(cidrStr) + if err != nil { + return fmt.Errorf("createHostRoute failed: %s", err) + } + } + } + + // Remove obsolete entries + for _, cidr := range prev { + cidrStr := cidr.String() + t.debugf("removing %s route", cidrStr) + _, err := t.deleteVPCRoute(cidrStr) + if err != nil { + return fmt.Errorf("deleteVPCRoute failed: %s", err) + } + if local { + err = t.deleteHostRoute(cidrStr) + if err != nil { + return fmt.Errorf("deleteHostRoute failed: %s", err) + } + } + } + + return nil +} + +func (t *AWSVPCTracker) createVPCRoute(cidr string) (*ec2.CreateRouteOutput, error) { + route := &ec2.CreateRouteInput{ + RouteTableId: &t.routeTableID, + InstanceId: &t.instanceID, + DestinationCidrBlock: &cidr, + } + return t.ec2.CreateRoute(route) +} + +func (t *AWSVPCTracker) createHostRoute(cidr string) error { + dst, err := parseCIDR(cidr) + if err != nil { + return err + } + route := &netlink.Route{ + LinkIndex: t.linkIndex, + Dst: dst, + Scope: netlink.SCOPE_LINK, + } + return netlink.RouteAdd(route) +} + +func (t *AWSVPCTracker) deleteVPCRoute(cidr string) (*ec2.DeleteRouteOutput, error) { + route := &ec2.DeleteRouteInput{ + RouteTableId: &t.routeTableID, + DestinationCidrBlock: &cidr, + } + return t.ec2.DeleteRoute(route) +} + +func (t *AWSVPCTracker) deleteHostRoute(cidr string) error { + dst, err := parseCIDR(cidr) + if err != nil { + return err + } + route := &netlink.Route{ + LinkIndex: t.linkIndex, + Dst: dst, + Scope: netlink.SCOPE_LINK, + } + return netlink.RouteDel(route) +} + +// detectRouteTableID detects AWS VPC Route Table ID of the given tracker instance. +func (t *AWSVPCTracker) detectRouteTableID() (*string, error) { + instancesParams := &ec2.DescribeInstancesInput{ + InstanceIds: []*string{aws.String(t.instanceID)}, + } + instancesResp, err := t.ec2.DescribeInstances(instancesParams) + if err != nil { + return nil, fmt.Errorf("DescribeInstances failed: %s", err) + } + if len(instancesResp.Reservations) == 0 || + len(instancesResp.Reservations[0].Instances) == 0 { + return nil, fmt.Errorf("cannot find %s instance within reservations", t.instanceID) + } + vpcID := instancesResp.Reservations[0].Instances[0].VpcId + subnetID := instancesResp.Reservations[0].Instances[0].SubnetId + + // First try to find a routing table associated with the subnet of the instance + tablesParams := &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("association.subnet-id"), + Values: []*string{subnetID}, + }, + }, + } + tablesResp, err := t.ec2.DescribeRouteTables(tablesParams) + if err != nil { + return nil, fmt.Errorf("DescribeRouteTables failed: %s", err) + } + if len(tablesResp.RouteTables) != 0 { + return tablesResp.RouteTables[0].RouteTableId, nil + } + // Fallback to the default routing table + tablesParams = &ec2.DescribeRouteTablesInput{ + Filters: []*ec2.Filter{ + { + Name: aws.String("association.main"), + Values: []*string{aws.String("true")}, + }, + { + Name: aws.String("vpc-id"), + Values: []*string{vpcID}, + }, + }, + } + tablesResp, err = t.ec2.DescribeRouteTables(tablesParams) + if err != nil { + return nil, fmt.Errorf("DescribeRouteTables failed: %s", err) + } + if len(tablesResp.RouteTables) != 0 { + return tablesResp.RouteTables[0].RouteTableId, nil + } + + return nil, fmt.Errorf("cannot find routetable for %s instance", t.instanceID) +} + +func (t *AWSVPCTracker) debugf(fmt string, args ...interface{}) { + common.Log.Debugf("[tracker] "+fmt, args...) +} + +func (t *AWSVPCTracker) infof(fmt string, args ...interface{}) { + common.Log.Infof("[tracker] "+fmt, args...) +} + +// Helpers + +// merge merges adjacent range entries. +// The given slice has to be sorted in increasing order. +func merge(r []address.Range) []address.Range { + var merged []address.Range + + for i := range r { + if prev := len(merged) - 1; prev >= 0 && merged[prev].End == r[i].Start { + merged[prev].End = r[i].End + } else { + merged = append(merged, r[i]) + } + } + + return merged +} + +// removeCommon filters out CIDR ranges which are contained in both a and b slices. +// Both slices have to be sorted in increasing order. +func removeCommon(a, b []address.CIDR) (newA, newB []address.CIDR) { + i, j := 0, 0 + + for i < len(a) && j < len(b) { + switch { + case a[i].Start() < b[j].Start() || a[i].End() < b[j].End(): + newA = append(newA, a[i]) + i++ + case a[i].Start() > b[j].Start() || a[i].End() > b[j].End(): + newB = append(newB, b[j]) + j++ + default: + i++ + j++ + } + + } + newA = append(newA, a[i:]...) + newB = append(newB, b[j:]...) + + return +} + +func parseCIDR(cidr string) (*net.IPNet, error) { + ip, ipnet, err := net.ParseCIDR(cidr) + if err != nil { + return nil, err + } + ipnet.IP = ip + + return ipnet, nil +} diff --git a/ipam/tracker/awsvpc_test.go b/ipam/tracker/awsvpc_test.go new file mode 100644 index 00000000..b99500a1 --- /dev/null +++ b/ipam/tracker/awsvpc_test.go @@ -0,0 +1,52 @@ +package tracker + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/weaveworks/weave/net/address" +) + +var ( + r0to127 = cidr("10.0.0.0", "10.0.0.127") + r128to255 = cidr("10.0.0.128", "10.0.0.255") + r0to255 = cidr("10.0.0.0", "10.0.0.255") + r1dot0to255 = cidr("10.0.1.0", "10.0.1.255") + r2dot0to255 = cidr("10.0.2.0", "10.0.2.255") +) + +func TestRemoveCommon(t *testing.T) { + a := []address.CIDR{r0to127, r1dot0to255} + b := []address.CIDR{r1dot0to255, r2dot0to255} + newA, newB := removeCommon(a, b) + require.Equal(t, []address.CIDR{r0to127}, newA) + require.Equal(t, []address.CIDR{r2dot0to255}, newB) +} + +func TestMerge(t *testing.T) { + ranges := []address.Range{ + r0to127.Range(), + r128to255.Range(), + r2dot0to255.Range(), + } + require.Equal(t, []address.Range{r0to255.Range(), r2dot0to255.Range()}, merge(ranges)) +} + +// Helper + +// TODO(mp) DRY with helpers of other tests. + +func ip(s string) address.Address { + addr, _ := address.ParseIP(s) + return addr +} + +// [start; end] +func cidr(start, end string) address.CIDR { + c := address.Range{Start: ip(start), End: ip(end) + 1}.CIDRs() + if len(c) != 1 { + panic("invalid cidr") + } + return c[0] +} diff --git a/net/address/address.go b/net/address/address.go index 6252adcd..d5fb8e00 100644 --- a/net/address/address.go +++ b/net/address/address.go @@ -125,6 +125,22 @@ func ParseCIDR(s string) (CIDR, error) { } } +func NewCIDRs(ranges []Range) (cidrs []CIDR) { + for _, r := range ranges { + cidrs = append(cidrs, r.CIDRs()...) + } + return cidrs +} + +func (cidr CIDR) Start() Address { + return cidr.Addr +} + +// cidr = [Start; End) +func (cidr CIDR) End() Address { + return cidr.Range().End +} + func (cidr CIDR) IsSubnet() bool { mask := cidr.Size() - 1 return Offset(cidr.Addr)&mask == 0 diff --git a/net/address/address_test.go b/net/address/address_test.go index 326a5b2e..8718fdb6 100644 --- a/net/address/address_test.go +++ b/net/address/address_test.go @@ -61,3 +61,9 @@ func TestCIDRs(t *testing.T) { []CIDR{cidr("10.0.0.1/32"), cidr("10.0.0.2/31"), cidr("10.0.0.4/30"), cidr("10.0.0.8/32")}, r.CIDRs()) } + +func TestCIDRStartAndEnd(t *testing.T) { + cidr, _ := ParseCIDR("10.0.0.0/24") + require.Equal(t, ip("10.0.0.0"), cidr.Start(), "") + require.Equal(t, ip("10.0.1.0"), cidr.End(), "") +} diff --git a/vendor/github.com/aws/aws-sdk-go b/vendor/github.com/aws/aws-sdk-go new file mode 160000 index 00000000..e4be72e5 --- /dev/null +++ b/vendor/github.com/aws/aws-sdk-go @@ -0,0 +1 @@ +Subproject commit e4be72e584418dc0ff2af22ae247095143a7f258 From 9fc7be83c83fb4897c49e6dca71b2476ed471546 Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 14:01:13 +0100 Subject: [PATCH 05/10] Create a Tracker when starting the router The tracker's HandleUpdate method is called by the ring update routines. In addition, we add a dummy overlay for AWSVPC to make `weave (status| report)` to produce reasonable reports which includes AWS hosts connection state. --- ipam/allocator.go | 22 +++++++++-- ipam/ring/ring.go | 38 +++++++++++++++++-- ipam/ring/ring_test.go | 86 ++++++++++++++++++++++-------------------- prog/weaver/main.go | 38 +++++++++++++++---- router/awsvpc.go | 75 ++++++++++++++++++++++++++++++++++++ router/bridge.go | 2 +- 6 files changed, 206 insertions(+), 55 deletions(-) create mode 100644 router/awsvpc.go diff --git a/ipam/allocator.go b/ipam/allocator.go index 50950ffe..c33fe920 100644 --- a/ipam/allocator.go +++ b/ipam/allocator.go @@ -14,6 +14,7 @@ import ( "github.com/weaveworks/weave/ipam/paxos" "github.com/weaveworks/weave/ipam/ring" "github.com/weaveworks/weave/ipam/space" + "github.com/weaveworks/weave/ipam/tracker" "github.com/weaveworks/weave/net/address" ) @@ -85,11 +86,14 @@ type Config struct { Quorum func() uint Db db.DB IsKnownPeer func(name mesh.PeerName) bool + Tracker tracker.LocalRangeTracker } // NewAllocator creates and initialises a new Allocator func NewAllocator(config Config) *Allocator { var participant paxos.Participant + var alloc *Allocator + var onUpdate ring.OnUpdate if config.IsObserver { participant = paxos.NewObserver() @@ -97,11 +101,19 @@ func NewAllocator(config Config) *Allocator { participant = paxos.NewNode(config.OurName, config.OurUID, 0) } - return &Allocator{ + if config.Tracker != nil { + onUpdate = func(prev []address.Range, curr []address.Range, local bool) { + if err := config.Tracker.HandleUpdate(prev, curr, local); err != nil { + alloc.errorf("HandleUpdate failed: %s", err) + } + } + } + + alloc = &Allocator{ ourName: config.OurName, seed: config.Seed, universe: config.Universe, - ring: ring.New(config.Universe.Range().Start, config.Universe.Range().End, config.OurName), + ring: ring.New(config.Universe.Range().Start, config.Universe.Range().End, config.OurName, onUpdate), owned: make(map[string]ownedData), db: config.Db, paxos: participant, @@ -111,6 +123,7 @@ func NewAllocator(config Config) *Allocator { dead: make(map[string]time.Time), now: time.Now, } + return alloc } func ParseCIDRSubnet(cidrStr string) (cidr address.CIDR, err error) { @@ -958,7 +971,7 @@ func (alloc *Allocator) loadPersistedData() bool { return false } - alloc.ring = persistedRing + alloc.ring.Restore(persistedRing) alloc.space.UpdateRanges(alloc.ring.OwnedRanges()) if ownedFound { @@ -1068,6 +1081,9 @@ func (alloc *Allocator) fatalf(fmt string, args ...interface{}) { func (alloc *Allocator) warnf(fmt string, args ...interface{}) { alloc.logf(common.Log.Warnf, fmt, args...) } +func (alloc *Allocator) errorf(fmt string, args ...interface{}) { + common.Log.Errorf("[allocator %s] "+fmt, append([]interface{}{alloc.ourName}, args...)...) +} func (alloc *Allocator) infof(fmt string, args ...interface{}) { alloc.logf(common.Log.Infof, fmt, args...) } diff --git a/ipam/ring/ring.go b/ipam/ring/ring.go index 37df88f2..01010f24 100644 --- a/ipam/ring/ring.go +++ b/ipam/ring/ring.go @@ -18,12 +18,15 @@ import ( "github.com/weaveworks/weave/net/address" ) +type OnUpdate func(prev, curr []address.Range, local bool) + // Ring represents the ring itself type Ring struct { Start, End address.Address // [min, max) tokens in this ring. Due to wrapping, min == max (effectively) Peer mesh.PeerName // name of peer owning this ring instance Entries entries // list of entries sorted by token Seeds []mesh.PeerName // peers with which the ring was seeded + onUpdate OnUpdate } func (r *Ring) assertInvariants() { @@ -94,15 +97,33 @@ func (r *Ring) checkEntries(entries entries) error { return nil } +func (r *Ring) trackUpdates() func() { + return r.trackUpdatesOfPeer(r.Peer) +} + +func (r *Ring) trackUpdatesOfPeer(peer mesh.PeerName) func() { + if r.onUpdate == nil { + return func() {} + } + ranges := r.OwnedRangesOfPeer(peer) + return func() { r.onUpdate(ranges, r.OwnedRangesOfPeer(peer), peer == r.Peer) } +} + // New creates an empty ring belonging to peer. -func New(start, end address.Address, peer mesh.PeerName) *Ring { +func New(start, end address.Address, peer mesh.PeerName, f OnUpdate) *Ring { common.Assert(start < end) - ring := &Ring{Start: start, End: end, Peer: peer, Entries: make([]*entry, 0)} + ring := &Ring{Start: start, End: end, Peer: peer, Entries: make([]*entry, 0), onUpdate: f} ring.updateExportedVariables() return ring } +func (r *Ring) Restore(other *Ring) { + onUpdate := r.onUpdate + *r = *other + r.onUpdate = onUpdate +} + func (r *Ring) Range() address.Range { return address.Range{Start: r.Start, End: r.End} } @@ -126,6 +147,7 @@ func (r *Ring) GrantRangeToHost(start, end address.Address, peer mesh.PeerName) //fmt.Printf("%s GrantRangeToHost [%v,%v) -> %s\n", r.Peer, start, end, peer) r.assertInvariants() + defer r.trackUpdates()() defer r.assertInvariants() defer r.updateExportedVariables() @@ -193,6 +215,7 @@ func (r *Ring) GrantRangeToHost(start, end address.Address, peer mesh.PeerName) // got updated as a result. func (r *Ring) Merge(gossip Ring) (bool, error) { r.assertInvariants() + defer r.trackUpdates()() defer r.updateExportedVariables() // Don't panic when checking the gossiped in ring. @@ -230,6 +253,7 @@ func (r *Ring) Merge(gossip Ring) (bool, error) { r.Seeds = gossip.Seeds } r.Entries = result + return updated, nil } @@ -336,10 +360,14 @@ func (r *Ring) splitRangesOverZero(ranges []address.Range) []address.Range { // ranges are owned by this peer. Will split ranges which // span 0 in the ring. func (r *Ring) OwnedRanges() (result []address.Range) { + return r.OwnedRangesOfPeer(r.Peer) +} + +func (r *Ring) OwnedRangesOfPeer(peer mesh.PeerName) (result []address.Range) { r.assertInvariants() for i, entry := range r.Entries { - if entry.Peer == r.Peer { + if entry.Peer == peer { nextEntry := r.Entries.entry(i + 1) result = append(result, address.Range{Start: entry.Token, End: nextEntry.Token}) } @@ -371,6 +399,8 @@ func (r *Ring) AllRangeInfo() (result []RangeInfo) { // in. Only works for empty rings. Each claimed range is CIDR-aligned. func (r *Ring) ClaimForPeers(peers []mesh.PeerName) { common.Assert(r.Empty()) + + defer r.trackUpdates()() defer r.assertInvariants() defer r.updateExportedVariables() defer func() { @@ -527,6 +557,8 @@ func (r *Ring) PickPeerForTransfer(isValid func(mesh.PeerName) bool) mesh.PeerNa // and return ranges indicating the new space we picked up func (r *Ring) Transfer(from, to mesh.PeerName) []address.Range { r.assertInvariants() + defer r.trackUpdates()() + defer r.trackUpdatesOfPeer(from)() defer r.assertInvariants() defer r.updateExportedVariables() diff --git a/ipam/ring/ring_test.go b/ipam/ring/ring_test.go index f360a088..fa5e0c44 100644 --- a/ipam/ring/ring_test.go +++ b/ipam/ring/ring_test.go @@ -36,8 +36,12 @@ func merge(r1, r2 *Ring) error { return err } +func NewRing(start, end address.Address, peer mesh.PeerName) *Ring { + return New(start, end, peer, nil) +} + func TestInvariants(t *testing.T) { - ring := New(start, end, peer1name) + ring := NewRing(start, end, peer1name) // Check ring is sorted ring.Entries = []*entry{{Token: dot245, Peer: peer1name}, {Token: dot10, Peer: peer2name}} @@ -48,7 +52,7 @@ func TestInvariants(t *testing.T) { require.True(t, ring.checkInvariants() == ErrTokenRepeated, "Expected error") // Check tokens are in bounds - ring = New(dot10, dot245, peer1name) + ring = NewRing(dot10, dot245, peer1name) ring.Entries = []*entry{{Token: start, Peer: peer1name}} require.True(t, ring.checkInvariants() == ErrTokenOutOfRange, "Expected error") @@ -57,7 +61,7 @@ func TestInvariants(t *testing.T) { } func TestInsert(t *testing.T) { - ring := New(start, end, peer1name) + ring := NewRing(start, end, peer1name) ring.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}} require.Panics(t, func() { @@ -82,7 +86,7 @@ func TestInsert(t *testing.T) { } func TestBetween(t *testing.T) { - ring1 := New(start, end, peer1name) + ring1 := NewRing(start, end, peer1name) ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}} // First off, in a ring where everything is owned by the peer @@ -124,8 +128,8 @@ func TestBetween(t *testing.T) { } func TestGrantSimple(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) // Claim everything for peer1 ring1.ClaimItAll() @@ -159,8 +163,8 @@ func TestGrantSimple(t *testing.T) { } func TestGrantSplit(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) // Claim everything for peer1 ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 256}} @@ -188,8 +192,8 @@ func TestGrantSplit(t *testing.T) { } func TestMergeSimple(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) // Claim everything for peer1 ring1.ClaimItAll() @@ -217,18 +221,18 @@ func TestMergeSimple(t *testing.T) { func TestMergeErrors(t *testing.T) { // Cannot Merge in an invalid ring - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) ring2.Entries = []*entry{{Token: middle, Peer: peer2name}, {Token: start, Peer: peer2name}} require.True(t, merge(ring1, ring2) == ErrNotSorted, "Expected ErrNotSorted") // Should Merge two rings for different ranges - ring2 = New(start, middle, peer2name) + ring2 = NewRing(start, middle, peer2name) ring2.Entries = []*entry{} require.True(t, merge(ring1, ring2) == ErrDifferentRange, "Expected ErrDifferentRange") // Cannot Merge newer version of entry I own - ring2 = New(start, end, peer2name) + ring2 = NewRing(start, end, peer2name) ring1.Entries = []*entry{{Token: start, Peer: peer1name}} ring2.Entries = []*entry{{Token: start, Peer: peer1name, Version: 1}} fmt.Println(merge(ring1, ring2)) @@ -246,8 +250,8 @@ func TestMergeErrors(t *testing.T) { } func TestMergeMore(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) assertRing := func(ring *Ring, entries entries) { require.Equal(t, entries, ring.Entries) @@ -297,8 +301,8 @@ func TestMergeMore(t *testing.T) { } func TestMergeSplit(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) // Claim everything for peer2 ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 256}} @@ -320,8 +324,8 @@ func TestMergeSplit(t *testing.T) { } func TestMergeSplit2(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) // Claim everything for peer2 ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 250}, {Token: dot250, Peer: peer2name, Free: 5}} @@ -344,8 +348,8 @@ func TestMergeSplit2(t *testing.T) { // A simple test, very similar to above, but using the marshalling to byte[]s func TestGossip(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) assertRing := func(ring *Ring, entries entries) { require.Equal(t, entries, ring.Entries) @@ -372,7 +376,7 @@ func assertPeersWithSpace(t *testing.T, ring *Ring, start, end address.Address, } func TestFindFree(t *testing.T) { - ring1 := New(start, end, peer1name) + ring1 := NewRing(start, end, peer1name) assertPeersWithSpace(t, ring1, start, end, 0) @@ -408,8 +412,8 @@ func TestFindFree(t *testing.T) { } func TestReportFree(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) ring1.ClaimItAll() ring1.GrantRangeToHost(middle, end, peer2name) @@ -423,7 +427,7 @@ func TestReportFree(t *testing.T) { } func TestMisc(t *testing.T) { - ring := New(start, end, peer1name) + ring := NewRing(start, end, peer1name) require.True(t, ring.Empty(), "empty") @@ -432,8 +436,8 @@ func TestMisc(t *testing.T) { } func TestEmptyGossip(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) ring1.ClaimItAll() // This used to panic, and it shouldn't @@ -441,8 +445,8 @@ func TestEmptyGossip(t *testing.T) { } func TestMergeOldMessage(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) ring1.ClaimItAll() require.NoError(t, merge(ring2, ring1)) @@ -452,8 +456,8 @@ func TestMergeOldMessage(t *testing.T) { } func TestSplitRangeAtBeginning(t *testing.T) { - ring1 := New(start, end, peer1name) - ring2 := New(start, end, peer2name) + ring1 := NewRing(start, end, peer1name) + ring2 := NewRing(start, end, peer2name) ring1.ClaimItAll() require.NoError(t, merge(ring2, ring1)) @@ -463,7 +467,7 @@ func TestSplitRangeAtBeginning(t *testing.T) { } func TestOwnedRange(t *testing.T) { - ring1 := New(start, end, peer1name) + ring1 := NewRing(start, end, peer1name) ring1.ClaimItAll() require.Equal(t, []address.Range{{Start: start, End: end}}, ring1.OwnedRanges()) @@ -471,7 +475,7 @@ func TestOwnedRange(t *testing.T) { ring1.GrantRangeToHost(middle, end, peer2name) require.Equal(t, []address.Range{{Start: start, End: middle}}, ring1.OwnedRanges()) - ring2 := New(start, end, peer2name) + ring2 := NewRing(start, end, peer2name) merge(ring2, ring1) require.Equal(t, []address.Range{{Start: middle, End: end}}, ring2.OwnedRanges()) @@ -486,7 +490,7 @@ func TestOwnedRange(t *testing.T) { func TestTransfer(t *testing.T) { // First test just checks if we can grant some range to a host, when we transfer it, we get it back - ring1 := New(start, end, peer1name) + ring1 := NewRing(start, end, peer1name) ring1.ClaimItAll() ring1.GrantRangeToHost(middle, end, peer2name) ring1.Transfer(peer2name, peer1name) @@ -494,7 +498,7 @@ func TestTransfer(t *testing.T) { // Second test is what happens when a token exists at the end of a range but is transferred // - does it get resurrected correctly? - ring1 = New(start, end, peer1name) + ring1 = NewRing(start, end, peer1name) ring1.ClaimItAll() ring1.GrantRangeToHost(middle, end, peer2name) ring1.Transfer(peer2name, peer1name) @@ -503,7 +507,7 @@ func TestTransfer(t *testing.T) { } func TestOwner(t *testing.T) { - ring1 := New(start, end, peer1name) + ring1 := NewRing(start, end, peer1name) require.True(t, ring1.Contains(start), "start should be in ring") require.False(t, ring1.Contains(end), "end should not be in ring") @@ -544,7 +548,7 @@ func TestClaimForPeers(t *testing.T) { peers := makePeers(numPeers) // Test for a range of peer counts for i := 0; i < numPeers; i++ { - ring := New(start, end, peers[0]) + ring := NewRing(start, end, peers[0]) ring.ClaimForPeers(peers[:i+1]) } } @@ -582,7 +586,7 @@ func TestFuzzRing(t *testing.T) { sort.Sort(addressSlice(tokens)) peer := peers[rand.Intn(len(peers))] - ring := New(start, end, peer) + ring := NewRing(start, end, peer) for _, token := range tokens { peer = peers[rand.Intn(len(peers))] ring.Entries = append(ring.Entries, &entry{Token: start + token, Peer: peer}) @@ -616,7 +620,7 @@ func TestFuzzRing(t *testing.T) { } peer := peers[rand.Intn(len(peers))] - ring := New(start, end, peer) + ring := NewRing(start, end, peer) for _, token := range tokens { peer = peers[rand.Intn(len(peers))] ring.Entries = append(ring.Entries, &entry{Token: start + token, Peer: peer}) @@ -654,7 +658,7 @@ func TestFuzzRingHard(t *testing.T) { common.Log.Debugf("%s: Adding peer", peer) nextPeerID++ peers = append(peers, peer) - rings = append(rings, New(start, end, peer)) + rings = append(rings, NewRing(start, end, peer)) } for i := 0; i < numPeers; i++ { diff --git a/prog/weaver/main.go b/prog/weaver/main.go index 2846fa29..a7e11718 100644 --- a/prog/weaver/main.go +++ b/prog/weaver/main.go @@ -22,6 +22,7 @@ import ( "github.com/weaveworks/weave/common/docker" "github.com/weaveworks/weave/db" "github.com/weaveworks/weave/ipam" + "github.com/weaveworks/weave/ipam/tracker" "github.com/weaveworks/weave/nameserver" weavenet "github.com/weaveworks/weave/net" "github.com/weaveworks/weave/net/address" @@ -170,6 +171,7 @@ func main() { datapathName string trustedSubnetStr string dbPrefix string + isAWSVPC bool defaultDockerHost = "unix:///var/run/docker.sock" ) @@ -208,6 +210,7 @@ func main() { mflag.StringVar(&datapathName, []string{"-datapath"}, "", "ODP datapath name") mflag.StringVar(&trustedSubnetStr, []string{"-trusted-subnets"}, "", "comma-separated list of trusted subnets in CIDR notation") mflag.StringVar(&dbPrefix, []string{"-db-prefix"}, "/weavedb/weave", "pathname/prefix of filename to store data") + mflag.BoolVar(&isAWSVPC, []string{"#awsvpc", "-awsvpc"}, false, "use AWS VPC for routing") // crude way of detecting that we probably have been started in a // container, with `weave launch` --> suppress misleading paths in @@ -250,7 +253,7 @@ func main() { networkConfig.PacketLogging = nopPacketLogging{} } - overlay, bridge := createOverlay(datapathName, ifaceName, config.Host, config.Port, bufSzMB) + overlay, bridge := createOverlay(datapathName, ifaceName, isAWSVPC, config.Host, config.Port, bufSzMB) networkConfig.Bridge = bridge name := peerName(routerName, bridge.Interface()) @@ -302,7 +305,15 @@ func main() { defaultSubnet address.CIDR ) if ipamConfig.Enabled() { - allocator, defaultSubnet = createAllocator(router, ipamConfig, db, isKnownPeer) + var t tracker.LocalRangeTracker + if isAWSVPC { + Log.Infoln("Creating AWSVPC LocalRangeTracker") + t, err = tracker.NewAWSVPCTracker() + if err != nil { + Log.Fatalf("Cannot create AWSVPC LocalRangeTracker: %s", err) + } + } + allocator, defaultSubnet = createAllocator(router, ipamConfig, db, t, isKnownPeer) observeContainers(allocator) ids, err := dockerCli.AllContainerIDs() checkFatal(err) @@ -388,10 +399,18 @@ func (nopPacketLogging) LogPacket(string, weave.PacketKey) { func (nopPacketLogging) LogForwardPacket(string, weave.ForwardPacketKey) { } -func createOverlay(datapathName string, ifaceName string, host string, port int, bufSzMB int) (weave.NetworkOverlay, weave.Bridge) { +func createOverlay(datapathName string, ifaceName string, isAWSVPC bool, host string, port int, bufSzMB int) (weave.NetworkOverlay, weave.Bridge) { overlay := weave.NewOverlaySwitch() var bridge weave.Bridge + var ignoreSleeve bool + switch { + case isAWSVPC: + vpc := weave.NewAWSVPC() + overlay.Add("awsvpc", vpc) + bridge = weave.NullBridge{} + // Currently, we do not support any overlay with AWSVPC + ignoreSleeve = true case datapathName != "" && ifaceName != "": Log.Fatal("At most one of --datapath and --iface must be specified.") case datapathName != "": @@ -409,13 +428,17 @@ func createOverlay(datapathName string, ifaceName string, host string, port int, default: bridge = weave.NullBridge{} } - sleeve := weave.NewSleeveOverlay(host, port) - overlay.Add("sleeve", sleeve) - overlay.SetCompatOverlay(sleeve) + + if !ignoreSleeve { + sleeve := weave.NewSleeveOverlay(host, port) + overlay.Add("sleeve", sleeve) + overlay.SetCompatOverlay(sleeve) + } + return overlay, bridge } -func createAllocator(router *weave.NetworkRouter, config ipamConfig, db db.DB, isKnownPeer func(mesh.PeerName) bool) (*ipam.Allocator, address.CIDR) { +func createAllocator(router *weave.NetworkRouter, config ipamConfig, db db.DB, track tracker.LocalRangeTracker, isKnownPeer func(mesh.PeerName) bool) (*ipam.Allocator, address.CIDR) { ipRange, err := ipam.ParseCIDRSubnet(config.IPRangeCIDR) checkFatal(err) defaultSubnet := ipRange @@ -437,6 +460,7 @@ func createAllocator(router *weave.NetworkRouter, config ipamConfig, db db.DB, i Quorum: func() uint { return determineQuorum(config.PeerCount, router) }, Db: db, IsKnownPeer: isKnownPeer, + Tracker: track, } allocator := ipam.NewAllocator(c) diff --git a/router/awsvpc.go b/router/awsvpc.go new file mode 100644 index 00000000..59c64c97 --- /dev/null +++ b/router/awsvpc.go @@ -0,0 +1,75 @@ +package router + +// A dummy overlay for the AWSVPC underlay to make `weave status` to return +// a valid information about peer connections. + +import ( + "github.com/weaveworks/mesh" +) + +// mesh.OverlayConnection + +type AWSVPCConnection struct { + establishedChan chan struct{} + errorChan chan error +} + +func (conn *AWSVPCConnection) Confirm() { + // We close the channel to notify mesh that the connection has been established. + close(conn.establishedChan) +} + +func (conn *AWSVPCConnection) EstablishedChannel() <-chan struct{} { + return conn.establishedChan +} + +func (conn *AWSVPCConnection) ErrorChannel() <-chan error { + return conn.errorChan +} + +func (conn *AWSVPCConnection) Stop() {} + +func (conn *AWSVPCConnection) ControlMessage(tag byte, msg []byte) { +} + +func (conn *AWSVPCConnection) DisplayName() string { + return "awsvpc" +} + +// OverlayForwarder + +func (conn *AWSVPCConnection) Forward(key ForwardPacketKey) FlowOp { + return DiscardingFlowOp{} +} + +type AWSVPC struct{} + +func NewAWSVPC() AWSVPC { + return AWSVPC{} +} + +// mesh.Overlay + +func (vpc AWSVPC) AddFeaturesTo(features map[string]string) {} + +func (vpc AWSVPC) PrepareConnection(params mesh.OverlayConnectionParams) (mesh.OverlayConnection, error) { + conn := &AWSVPCConnection{ + establishedChan: make(chan struct{}), + errorChan: make(chan error, 1), + } + return conn, nil +} + +func (vpc AWSVPC) Diagnostics() interface{} { + return nil +} + +// NetworkOverlay + +func (vpc AWSVPC) InvalidateRoutes() {} + +func (vpc AWSVPC) InvalidateShortIDs() {} + +func (vpc AWSVPC) StartConsumingPackets(localPeer *mesh.Peer, peers *mesh.Peers, consumer OverlayConsumer) error { + return nil +} diff --git a/router/bridge.go b/router/bridge.go index 434669bd..3f8a193f 100644 --- a/router/bridge.go +++ b/router/bridge.go @@ -36,7 +36,7 @@ func (NullBridge) Interface() *net.Interface { } func (NullBridge) String() string { - return "" + return "no overlay bridge" } func (NullBridge) Stats() map[string]int { From de2a70861ce7ff19b75856599f4b6eaac26e2828 Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 14:07:21 +0100 Subject: [PATCH 06/10] Add GET /ipinfo/tracker The endpoint returns the name of tracker. It is used to determine whether the router is running a tracker. --- api/ipam.go | 8 ++++++++ ipam/http.go | 6 +++++- ipam/http_test.go | 2 +- prog/weaver/main.go | 4 +++- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/api/ipam.go b/api/ipam.go index 3f15af7f..d3463906 100644 --- a/api/ipam.go +++ b/api/ipam.go @@ -53,6 +53,14 @@ func (client *Client) DefaultSubnet() (*net.IPNet, error) { return ipnet, err } +func (client *Client) LocalRangeTracker() (string, error) { + t, err := client.httpVerb("GET", fmt.Sprintf("/ipinfo/tracker"), nil) + if err != nil { + return "", err + } + return t, nil +} + func parseIP(body string) (*net.IPNet, error) { ip, ipnet, err := net.ParseCIDR(string(body)) if err != nil { diff --git a/ipam/http.go b/ipam/http.go index 0d68052a..04eb2088 100644 --- a/ipam/http.go +++ b/ipam/http.go @@ -87,7 +87,7 @@ func (alloc *Allocator) handleHTTPClaim(dockerCli *docker.Client, w http.Respons } // HandleHTTP wires up ipams HTTP endpoints to the provided mux. -func (alloc *Allocator) HandleHTTP(router *mux.Router, defaultSubnet address.CIDR, dockerCli *docker.Client) { +func (alloc *Allocator) HandleHTTP(router *mux.Router, defaultSubnet address.CIDR, tracker string, dockerCli *docker.Client) { router.Methods("GET").Path("/ipinfo/defaultsubnet").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintf(w, "%s", defaultSubnet) }) @@ -174,4 +174,8 @@ func (alloc *Allocator) HandleHTTP(router *mux.Router, defaultSubnet address.CID transferred := alloc.AdminTakeoverRanges(ident) fmt.Fprintf(w, "%d IPs taken over from %s\n", transferred, ident) }) + + router.Methods("GET").Path("/ipinfo/tracker").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintf(w, tracker) + }) } diff --git a/ipam/http_test.go b/ipam/http_test.go index 176da222..c371949b 100644 --- a/ipam/http_test.go +++ b/ipam/http_test.go @@ -43,7 +43,7 @@ func listenHTTP(alloc *Allocator, subnet address.CIDR) int { router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, fmt.Sprintln(alloc)) }) - alloc.HandleHTTP(router, subnet, nil) + alloc.HandleHTTP(router, subnet, "", nil) httpListener, err := net.Listen("tcp", ":0") if err != nil { diff --git a/prog/weaver/main.go b/prog/weaver/main.go index a7e11718..798a1f5a 100644 --- a/prog/weaver/main.go +++ b/prog/weaver/main.go @@ -303,6 +303,7 @@ func main() { var ( allocator *ipam.Allocator defaultSubnet address.CIDR + trackerName string ) if ipamConfig.Enabled() { var t tracker.LocalRangeTracker @@ -312,6 +313,7 @@ func main() { if err != nil { Log.Fatalf("Cannot create AWSVPC LocalRangeTracker: %s", err) } + trackerName = "awsvpc" } allocator, defaultSubnet = createAllocator(router, ipamConfig, db, t, isKnownPeer) observeContainers(allocator) @@ -344,7 +346,7 @@ func main() { if httpAddr != "" { muxRouter := mux.NewRouter() if allocator != nil { - allocator.HandleHTTP(muxRouter, defaultSubnet, dockerCli) + allocator.HandleHTTP(muxRouter, defaultSubnet, trackerName, dockerCli) } if ns != nil { ns.HandleHTTP(muxRouter, dockerCli) From 7670fed72c110346cdc5927dd32be40b7803a084 Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 14:09:45 +0100 Subject: [PATCH 07/10] Add AWSVPC setup routines to the attach script When running in the AWSVPC mode, we need to do the following: - Keep the HW TX offloading on. - Check that the allocated address belongs to the default subnet, because only one subnet is allowed and it should be the default one. If it is not possible to determine whether AWSVPC is on, we ignore all the steps, because the attachment routines have to work regardless whether the router is running and the AWSVPC detection is done by querying the /ipinfo/tracker endpoint served by the router. --- net/veth.go | 8 +++---- plugin/net/cni.go | 2 +- plugin/net/driver.go | 3 ++- prog/weaveutil/attach.go | 45 +++++++++++++++++++++++++++++++++++++++- 4 files changed, 51 insertions(+), 7 deletions(-) diff --git a/net/veth.go b/net/veth.go index eea00485..fbca71c3 100644 --- a/net/veth.go +++ b/net/veth.go @@ -13,7 +13,7 @@ import ( ) // create and attach a veth to the Weave bridge -func CreateAndAttachVeth(name, peerName, bridgeName string, mtu int, init func(peer netlink.Link) error) (*netlink.Veth, error) { +func CreateAndAttachVeth(name, peerName, bridgeName string, mtu int, keepTXOn bool, init func(peer netlink.Link) error) (*netlink.Veth, error) { bridge, err := netlink.LinkByName(bridgeName) if err != nil { return nil, fmt.Errorf(`bridge "%s" not present; did you launch weave?`, bridgeName) @@ -42,7 +42,7 @@ func CreateAndAttachVeth(name, peerName, bridgeName string, mtu int, init func(p if err := netlink.LinkSetMasterByIndex(veth, bridge.Attrs().Index); err != nil { return cleanup(`unable to set master of %s: %s`, name, err) } - if bridgeType == Bridge { + if bridgeType == Bridge && !keepTXOn { if err := EthtoolTXOff(peerName); err != nil { return cleanup(`unable to set tx off on %q: %s`, peerName, err) } @@ -111,14 +111,14 @@ func interfaceExistsInNamespace(ns netns.NsHandle, ifName string) bool { return err == nil } -func AttachContainer(ns netns.NsHandle, id, ifName, bridgeName string, mtu int, withMulticastRoute bool, cidrs []*net.IPNet) error { +func AttachContainer(ns netns.NsHandle, id, ifName, bridgeName string, mtu int, withMulticastRoute bool, cidrs []*net.IPNet, keepTXOn bool) error { if !interfaceExistsInNamespace(ns, ifName) { maxIDLen := IFNAMSIZ - 1 - len(vethPrefix+"pl") if len(id) > maxIDLen { id = id[:maxIDLen] // trim passed ID if too long } name, peerName := vethPrefix+"pl"+id, vethPrefix+"pg"+id - _, err := CreateAndAttachVeth(name, peerName, bridgeName, mtu, func(veth netlink.Link) error { + _, err := CreateAndAttachVeth(name, peerName, bridgeName, mtu, keepTXOn, func(veth netlink.Link) error { if err := netlink.LinkSetNsFd(veth, int(ns)); err != nil { return fmt.Errorf("failed to move veth to container netns: %s", err) } diff --git a/plugin/net/cni.go b/plugin/net/cni.go index f43ef967..eddd7dc7 100644 --- a/plugin/net/cni.go +++ b/plugin/net/cni.go @@ -92,7 +92,7 @@ func (c *CNIPlugin) CmdAdd(args *skel.CmdArgs) error { id = fmt.Sprintf("%x", data) } - if err := weavenet.AttachContainer(ns, id, args.IfName, conf.BrName, conf.MTU, false, []*net.IPNet{&result.IP4.IP}); err != nil { + if err := weavenet.AttachContainer(ns, id, args.IfName, conf.BrName, conf.MTU, false, []*net.IPNet{&result.IP4.IP}, false); err != nil { return err } if err := weavenet.WithNetNSLink(ns, args.IfName, func(link netlink.Link) error { diff --git a/plugin/net/driver.go b/plugin/net/driver.go index 675ca93d..d8132529 100644 --- a/plugin/net/driver.go +++ b/plugin/net/driver.go @@ -134,8 +134,9 @@ func (driver *driver) JoinEndpoint(j *api.JoinRequest) (*api.JoinResponse, error if err != nil { return nil, driver.error("JoinEndpoint", "unable to find network info: %s", err) } + name, peerName := vethPair(j.EndpointID) - if _, err := weavenet.CreateAndAttachVeth(name, peerName, weavenet.WeaveBridgeName, 0, nil); err != nil { + if _, err := weavenet.CreateAndAttachVeth(name, peerName, weavenet.WeaveBridgeName, 0, false, nil); err != nil { return nil, driver.error("JoinEndpoint", "%s", err) } diff --git a/prog/weaveutil/attach.go b/prog/weaveutil/attach.go index 2dbe037f..83a3085a 100644 --- a/prog/weaveutil/attach.go +++ b/prog/weaveutil/attach.go @@ -3,12 +3,15 @@ package main import ( "fmt" "net" + "os" "strconv" "syscall" docker "github.com/fsouza/go-dockerclient" "github.com/vishvananda/netns" + "github.com/weaveworks/weave/api" + "github.com/weaveworks/weave/common" weavenet "github.com/weaveworks/weave/net" ) @@ -17,11 +20,26 @@ func attach(args []string) error { cmdUsage("attach-container", "[--no-multicast-route] ...") } + client := api.NewClient(os.Getenv("WEAVE_HTTP_ADDR"), common.Log) + keepTXOn := false + isAWSVPC := false + // In a case of an error, we skip applying necessary steps for AWSVPC, because + // "attach" should work without the weave router running. + if t, err := client.LocalRangeTracker(); err != nil { + fmt.Fprintf(os.Stderr, "unable to determine tracker: %s; skipping AWSVPC initialization\n", err) + } else if t == "awsvpc" { + isAWSVPC = true + keepTXOn = true + } + withMulticastRoute := true if args[0] == "--no-multicast-route" { withMulticastRoute = false args = args[1:] } + if isAWSVPC { + withMulticastRoute = false + } pid, nsContainer, err := containerPidAndNs(args[0]) if err != nil { @@ -40,7 +58,22 @@ func attach(args []string) error { if err != nil { return err } - err = weavenet.AttachContainer(nsContainer, fmt.Sprint(pid), weavenet.VethName, args[1], mtu, withMulticastRoute, cidrs) + + if isAWSVPC { + // Currently, we allow only IP addresses from the default subnet + if defaultSubnet, err := client.DefaultSubnet(); err != nil { + fmt.Fprintf(os.Stderr, "unable to retrieve default subnet: %s; skipping AWSVPC checks\n", err) + } else { + for _, cidr := range cidrs { + if !sameSubnet(cidr, defaultSubnet) { + format := "AWSVPC constraints violation: %s does not belong to the default subnet %s" + return fmt.Errorf(format, cidr, defaultSubnet) + } + } + } + } + + err = weavenet.AttachContainer(nsContainer, fmt.Sprint(pid), weavenet.VethName, args[1], mtu, withMulticastRoute, cidrs, keepTXOn) // If we detected an error but the container has died, tell the user that instead. if err != nil && !processExists(pid) { err = fmt.Errorf("Container %s died", args[0]) @@ -48,6 +81,16 @@ func attach(args []string) error { return err } +// sameSubnet checks whether ip belongs to network's subnet +func sameSubnet(ip *net.IPNet, network *net.IPNet) bool { + if network.Contains(ip.IP) { + i1, i2 := ip.Mask.Size() + n1, n2 := network.Mask.Size() + return i1 == n1 && i2 == n2 + } + return false +} + func containerPidAndNs(containerID string) (int, netns.NsHandle, error) { c, err := docker.NewVersionedClientFromEnv("1.18") if err != nil { From fb9b4ddec06c2552564abd8154889910abea2181 Mon Sep 17 00:00:00 2001 From: Martynas Pumputis Date: Tue, 7 Jun 2016 14:14:53 +0100 Subject: [PATCH 08/10] Enable AWSVPC This requires the following additional steps: - Setting proxy_arp on on the bridge, because that's how we do the routing (check the comment at the place where we set it on). - Disable ARP requests queueing, because it introduces unnecessary delay for the first ARP request resulting in a high latency of the first packet (on t2.micro machines I've encountered ~700ms latency). Also, the queue is bounded, so we might risking to drop the requests. --- weave | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/weave b/weave index 53af6c0d..1aab3d26 100755 --- a/weave +++ b/weave @@ -181,6 +181,7 @@ exec_remote() { -e PROXY_HOST="$PROXY_HOST" \ -e COVERAGE \ -e CHECKPOINT_DISABLE \ + -e AWSVPC \ $WEAVEEXEC_DOCKER_ARGS $EXEC_IMAGE --local "$@" } @@ -532,9 +533,10 @@ random_mac() { util_op() { if command_exists weaveutil ; then - weaveutil "$@" + WEAVE_HTTP_ADDR=$WEAVE_HTTP_ADDR weaveutil "$@" else docker run --rm --privileged --net=host --pid=host $(docker_sock_options) \ + -e WEAVE_HTTP_ADDR \ --entrypoint=/usr/bin/weaveutil $EXEC_IMAGE "$@" fi } @@ -645,7 +647,7 @@ EOF fi fi - [ "$1" = "--without-ethtool" ] || ethtool_tx_off_$BRIDGE_TYPE $BRIDGE + [ "$1" = "--without-ethtool" -o -n "$AWSVPC" ] || ethtool_tx_off_$BRIDGE_TYPE $BRIDGE ip link set dev $BRIDGE up @@ -660,6 +662,12 @@ expose_ip() { if ! ip addr show dev $BRIDGE | grep -qF $CIDR ; then ip addr add dev $BRIDGE $CIDR arp_update $BRIDGE $CIDR || true + # Remove a default route installed by the kernel, because awsvpc + # has installed it as well + if [ -n "$AWSVPC" ]; then + RCIDR=$(ip route list exact $CIDR proto kernel | head -n1 | cut -d' ' -f1) + [ -n "$RCIDR" ] && ip route del dev $BRIDGE proto kernel $RCIDR + fi fi [ -z "$FQDN" ] || when_weave_running put_dns_fqdn_no_check_alive weave:expose $FQDN $CIDR done @@ -1501,22 +1509,19 @@ launch_router() { LAUNCHING_ROUTER=1 check_forwarding_rules enforce_docker_bridge_addr_assign_type - create_bridge - docker_bridge_ip - # We set the router name to the bridge MAC, which in turn is - # derived from the system UUID (if available), and thus stable - # across reboots. - PEERNAME=$(cat /sys/class/net/$BRIDGE/address) + # backward compatibility... if is_cidr "$1" ; then echo "WARNING: $1 parameter ignored; 'weave launch' no longer takes a CIDR as the first parameter" >&2 shift 1 fi + CONTAINER_PORT=$PORT ARGS= IPRANGE= IPRANGE_SPECIFIED= + docker_bridge_ip DNS_ROUTER_OPTS="--dns-listen-address $DOCKER_BRIDGE_IP:53" NO_DNS_OPT= @@ -1558,6 +1563,10 @@ launch_router() { --no-restart) RESTART_POLICY= ;; + --awsvpc) + AWSVPC_ARGS="--awsvpc" + AWSVPC=1 + ;; *) ARGS="$ARGS '$(echo "$1" | sed "s|'|'\"'\"'|g")'" ;; @@ -1565,6 +1574,13 @@ launch_router() { shift done eval "set -- $ARGS" + + create_bridge + # We set the router name to the bridge MAC, which in turn is + # derived from the system UUID (if available), and thus stable + # across reboots. + PEERNAME=$(cat /sys/class/net/$BRIDGE/address) + if [ -z "$IPRANGE_SPECIFIED" ] ; then IPRANGE="10.32.0.0/12" if ! check_overlap $IPRANGE ; then @@ -1601,11 +1617,23 @@ launch_router() { --ipalloc-range "$IPRANGE" \ --dns-effective-listen-address $DOCKER_BRIDGE_IP \ $DNS_ROUTER_OPTS $NO_DNS_OPT \ + $AWSVPC_ARGS \ --http-addr $HTTP_ADDR \ "$@") setup_router_iface_$BRIDGE_TYPE wait_for_status $CONTAINER_NAME http_call $HTTP_ADDR populate_router + if [ -n "$AWSVPC" ]; then + expose_ip + # Set proxy_arp on the bridge, so that it could accept packets destined + # to containers within the same subnet but running on remote hosts. + # Without it, exact routes on each container are required. + echo 1 >/proc/sys/net/ipv4/conf/$BRIDGE/proxy_arp + # Avoid delaying the first ARP request. Also, setting it to 0 avoids + # placing the request into a bounded queue as it can be seen: + # https://git.kernel.org/cgit/linux/kernel/git/stable/linux-stable.git/tree/net/ipv4/arp.c?id=refs/tags/v4.6.1#n819 + echo 0 >/proc/sys/net/ipv4/neigh/$BRIDGE/proxy_delay + fi } # Recreate the parameter values that are set when the router is first launched From 37fd54f20b33ca61963df20b5f76fec2f6bc0356 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 7 Jun 2016 16:01:44 +0100 Subject: [PATCH 09/10] Perform AWSVPC checks for attach in script rather than in weaveutil --- api/ipam.go | 8 ------ prog/weaveutil/attach.go | 57 +++++++++------------------------------- weave | 17 ++++++++++-- 3 files changed, 27 insertions(+), 55 deletions(-) diff --git a/api/ipam.go b/api/ipam.go index d3463906..3f15af7f 100644 --- a/api/ipam.go +++ b/api/ipam.go @@ -53,14 +53,6 @@ func (client *Client) DefaultSubnet() (*net.IPNet, error) { return ipnet, err } -func (client *Client) LocalRangeTracker() (string, error) { - t, err := client.httpVerb("GET", fmt.Sprintf("/ipinfo/tracker"), nil) - if err != nil { - return "", err - } - return t, nil -} - func parseIP(body string) (*net.IPNet, error) { ip, ipnet, err := net.ParseCIDR(string(body)) if err != nil { diff --git a/prog/weaveutil/attach.go b/prog/weaveutil/attach.go index 83a3085a..c3c469de 100644 --- a/prog/weaveutil/attach.go +++ b/prog/weaveutil/attach.go @@ -3,42 +3,33 @@ package main import ( "fmt" "net" - "os" "strconv" "syscall" docker "github.com/fsouza/go-dockerclient" "github.com/vishvananda/netns" - "github.com/weaveworks/weave/api" - "github.com/weaveworks/weave/common" weavenet "github.com/weaveworks/weave/net" ) func attach(args []string) error { if len(args) < 4 { - cmdUsage("attach-container", "[--no-multicast-route] ...") + cmdUsage("attach-container", "[--no-multicast-route] [--keep-tx-on] ...") } - client := api.NewClient(os.Getenv("WEAVE_HTTP_ADDR"), common.Log) keepTXOn := false - isAWSVPC := false - // In a case of an error, we skip applying necessary steps for AWSVPC, because - // "attach" should work without the weave router running. - if t, err := client.LocalRangeTracker(); err != nil { - fmt.Fprintf(os.Stderr, "unable to determine tracker: %s; skipping AWSVPC initialization\n", err) - } else if t == "awsvpc" { - isAWSVPC = true - keepTXOn = true - } - withMulticastRoute := true - if args[0] == "--no-multicast-route" { - withMulticastRoute = false - args = args[1:] - } - if isAWSVPC { - withMulticastRoute = false + for i := 0; i < len(args); { + switch args[i] { + case "--no-multicast-route": + withMulticastRoute = false + args = append(args[:i], args[i+1:]...) + case "--keep-tx-on": + keepTXOn = true + args = append(args[:i], args[i+1:]...) + default: + i++ + } } pid, nsContainer, err := containerPidAndNs(args[0]) @@ -59,20 +50,6 @@ func attach(args []string) error { return err } - if isAWSVPC { - // Currently, we allow only IP addresses from the default subnet - if defaultSubnet, err := client.DefaultSubnet(); err != nil { - fmt.Fprintf(os.Stderr, "unable to retrieve default subnet: %s; skipping AWSVPC checks\n", err) - } else { - for _, cidr := range cidrs { - if !sameSubnet(cidr, defaultSubnet) { - format := "AWSVPC constraints violation: %s does not belong to the default subnet %s" - return fmt.Errorf(format, cidr, defaultSubnet) - } - } - } - } - err = weavenet.AttachContainer(nsContainer, fmt.Sprint(pid), weavenet.VethName, args[1], mtu, withMulticastRoute, cidrs, keepTXOn) // If we detected an error but the container has died, tell the user that instead. if err != nil && !processExists(pid) { @@ -81,16 +58,6 @@ func attach(args []string) error { return err } -// sameSubnet checks whether ip belongs to network's subnet -func sameSubnet(ip *net.IPNet, network *net.IPNet) bool { - if network.Contains(ip.IP) { - i1, i2 := ip.Mask.Size() - n1, n2 := network.Mask.Size() - return i1 == n1 && i2 == n2 - } - return false -} - func containerPidAndNs(containerID string) (int, netns.NsHandle, error) { c, err := docker.NewVersionedClientFromEnv("1.18") if err != nil { diff --git a/weave b/weave index 1aab3d26..419d9905 100755 --- a/weave +++ b/weave @@ -533,10 +533,9 @@ random_mac() { util_op() { if command_exists weaveutil ; then - WEAVE_HTTP_ADDR=$WEAVE_HTTP_ADDR weaveutil "$@" + weaveutil "$@" else docker run --rm --privileged --net=host --pid=host $(docker_sock_options) \ - -e WEAVE_HTTP_ADDR \ --entrypoint=/usr/bin/weaveutil $EXEC_IMAGE "$@" fi } @@ -894,6 +893,8 @@ container_in_host_ns() { attach() { ATTACH_ARGS="" [ -n "$NO_MULTICAST_ROUTE" ] && ATTACH_ARGS="--no-multicast-route" + # Relying on AWSVPC being set in 'ipam_cidrs allocate', except for 'weave restart' + [ -n "$AWSVPC" ] && ATTACH_ARGS="--no-multicast-route --keep-tx-on" util_op attach-container $ATTACH_ARGS $CONTAINER $BRIDGE $MTU "$@" } @@ -1269,6 +1270,12 @@ ipam_reclaim_no_check_alive() { done } +detect_awsvpc() { + # Ignoring errors here: if we cannot detect AWSVPC we will skip the relevant + # steps, because "attach" should work without the weave router running. + [ "$(call_weave GET /ipinfo/tracker)" != "awsvpc" ] || AWSVPC=1 +} + # Call IPAM as necessary to lookup or allocate addresses # # $1 is one of 'lookup', 'allocate' or 'allocate_no_check_alive', $2 @@ -1285,6 +1292,11 @@ ipam_cidrs() { allocate) METHOD=POST CHECK_ALIVE="?check-alive=true" + detect_awsvpc + if [ -n "$AWSVPC" -a $# -gt 2 ] ; then + echo "Error: no IP addresses or subnets may be specified in AWSVPC mode" >&2 + return 1 + fi ;; allocate_no_check_alive) METHOD=POST @@ -2098,6 +2110,7 @@ EOF for CIDR in $ALL_CIDRS ; do call_weave PUT /ip/$CONTAINER/$CIDR?check-alive=true done + detect_awsvpc do_or_die $CONTAINER attach $ALL_CIDRS when_weave_running with_container_fqdn $CONTAINER put_dns_fqdn $ALL_CIDRS echo $RES From c77b2e8fb33b3f1841a6d3f9ab439480e7c5f195 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Tue, 7 Jun 2016 18:02:15 +0100 Subject: [PATCH 10/10] Tell the proxy that multicast is off in awsvpc mode. Otherwise it will use the regular weavewait program which wait for the multicast route to be added. --- weave | 2 ++ 1 file changed, 2 insertions(+) diff --git a/weave b/weave index 419d9905..b09bd849 100755 --- a/weave +++ b/weave @@ -1689,6 +1689,8 @@ launch_proxy() { docker create -v /w -v /w-noop -v /w-nomcast --name=$VOLUMES_CONTAINER_NAME \ --label=weavevolumes --entrypoint=/bin/false $EXEC_IMAGE >/dev/null fi + detect_awsvpc + [ -n "$AWSVPC" ] && PROXY_ARGS="$PROXY_ARGS --no-multicast-route" PROXY_CONTAINER=$(docker run -d --name=$PROXY_CONTAINER_NAME \ $(docker_run_options) \ $RESTART_POLICY \