From 4f3dcf8dc90f1d1ec2fd7f05fc571badbcb658f0 Mon Sep 17 00:00:00 2001 From: Tom Wilkie Date: Wed, 15 Apr 2015 18:02:34 +0100 Subject: [PATCH 1/7] Ring CRDT for IP Allocation Management The basic data structure used to share information about IP allocations between peers --- ipam/ring/entry.go | 102 ++++++ ipam/ring/expvar.go | 31 ++ ipam/ring/ring.go | 521 ++++++++++++++++++++++++++++++ ipam/ring/ring_test.go | 713 +++++++++++++++++++++++++++++++++++++++++ ipam/utils/utils.go | 49 +++ testing/util.go | 55 ++++ 6 files changed, 1471 insertions(+) create mode 100644 ipam/ring/entry.go create mode 100644 ipam/ring/expvar.go create mode 100644 ipam/ring/ring.go create mode 100644 ipam/ring/ring_test.go create mode 100644 ipam/utils/utils.go diff --git a/ipam/ring/entry.go b/ipam/ring/entry.go new file mode 100644 index 00000000..358216ec --- /dev/null +++ b/ipam/ring/entry.go @@ -0,0 +1,102 @@ +package ring + +import ( + "sort" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" + "github.com/weaveworks/weave/router" +) + +// Entry represents entries around the ring +type entry struct { + Token address.Address // The start of this range + Peer router.PeerName // Who owns this range + Version uint32 // Version of this range + Free address.Offset // Number of free IPs in this range +} + +func (e *entry) Equal(e2 *entry) bool { + return e.Token == e2.Token && e.Peer == e2.Peer && + e.Version == e2.Version +} + +func (e *entry) update(peername router.PeerName, free address.Offset) { + e.Peer = peername + e.Version++ + e.Free = free +} + +// For compatibility with sort.Interface +type entries []*entry + +func (es entries) Len() int { return len(es) } +func (es entries) Less(i, j int) bool { return es[i].Token < es[j].Token } +func (es entries) Swap(i, j int) { panic("Should never be swapping entries!") } + +func (es entries) entry(i int) *entry { + i = i % len(es) + if i < 0 { + i += len(es) + } + return es[i] +} + +func (es *entries) insert(e entry) { + i := sort.Search(len(*es), func(j int) bool { + return (*es)[j].Token >= e.Token + }) + + if i < len(*es) && (*es)[i].Token == e.Token { + panic("Trying to insert an existing token!") + } + + *es = append(*es, &entry{}) + copy((*es)[i+1:], (*es)[i:]) + (*es)[i] = &e +} + +func (es entries) get(token address.Address) (*entry, bool) { + i := sort.Search(len(es), func(j int) bool { + return es[j].Token >= token + }) + + if i < len(es) && es[i].Token == token { + return es[i], true + } + + return nil, false +} + +func (es *entries) remove(i int) { + *es = (*es)[:i+copy((*es)[i:], (*es)[i+1:])] +} + +// Is token between entries at i and j? +// NB i and j can overflow and will wrap +// NBB this function does not work very well if there is only one +// token on the ring; luckily an accurate answer is not needed +// by the call sites in this case. +func (es entries) between(token address.Address, i, j int) bool { + common.Assert(i < j) + + first := es.entry(i) + second := es.entry(j) + + switch { + case first.Token == second.Token: + // This implies there is only one token + // on the ring (i < j and i.token == j.token) + // In which case everything is between, expect + // this one token + return token != first.Token + + case first.Token < second.Token: + return first.Token <= token && token < second.Token + + case second.Token < first.Token: + return first.Token <= token || token < second.Token + } + + panic("Should never get here - switch covers all possibilities.") +} diff --git a/ipam/ring/expvar.go b/ipam/ring/expvar.go new file mode 100644 index 00000000..9ba8c8ea --- /dev/null +++ b/ipam/ring/expvar.go @@ -0,0 +1,31 @@ +package ring + +/* Exported variables for monitoring and management */ + +import ( + "expvar" + "strconv" +) + +var ( + expRingSize = expvar.NewMap("ipam.ringSize") + expRingEntries = expvar.NewMap("ipam.ringEntries") +) + +type _int int + +func (i _int) String() string { + return strconv.Itoa(int(i)) +} + +type _uint32 uint32 + +func (i _uint32) String() string { + return strconv.FormatUint(uint64(i), 10) +} + +func (r *Ring) updateExportedVariables() { + ringName := r.Start.String() + expRingSize.Set(ringName, _uint32(r.End-r.Start)) + expRingEntries.Set(ringName, _int(len(r.Entries))) +} diff --git a/ipam/ring/ring.go b/ipam/ring/ring.go new file mode 100644 index 00000000..e4780300 --- /dev/null +++ b/ipam/ring/ring.go @@ -0,0 +1,521 @@ +/* +Package ring implements a simple ring CRDT. +*/ +package ring + +import ( + "bytes" + "errors" + "fmt" + "io" + "math/rand" + "sort" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" + "github.com/weaveworks/weave/router" +) + +// 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 router.PeerName // name of peer owning this ring instance + Entries entries // list of entries sorted by token +} + +func (r *Ring) assertInvariants() { + err := r.checkInvariants() + if err != nil { + panic(err.Error()) + } +} + +// Errors returned by Merge +var ( + ErrNotSorted = errors.New("Ring not sorted") + ErrTokenRepeated = errors.New("Token appears twice in ring") + ErrTokenOutOfRange = errors.New("Token is out of range") + ErrDifferentSubnets = errors.New("IP Allocator with different subnet detected") + ErrNewerVersion = errors.New("Received new version for entry I own!") + ErrInvalidEntry = errors.New("Received invalid state update!") + ErrEntryInMyRange = errors.New("Received new entry in my range!") + ErrNoFreeSpace = errors.New("No free space found!") + ErrTooMuchFreeSpace = errors.New("Entry reporting too much free space!") + ErrInvalidTimeout = errors.New("dt must be greater than 0") + ErrNotFound = errors.New("No entries for peer found") +) + +func (r *Ring) checkInvariants() error { + if !sort.IsSorted(r.Entries) { + return ErrNotSorted + } + + // Check no token appears twice + // We know it's sorted... + for i := 1; i < len(r.Entries); i++ { + if r.Entries[i-1].Token == r.Entries[i].Token { + return ErrTokenRepeated + } + } + + if len(r.Entries) == 0 { + return nil + } + + // Check tokens are in range + if r.Entries.entry(0).Token < r.Start { + return ErrTokenOutOfRange + } + if r.Entries.entry(-1).Token >= r.End { + return ErrTokenOutOfRange + } + + // Check all the freespaces are in range + for i, entry := range r.Entries { + next := r.Entries.entry(i + 1) + distance := r.distance(entry.Token, next.Token) + + if entry.Free > distance { + return ErrTooMuchFreeSpace + } + } + + return nil +} + +// New creates an empty ring belonging to peer. +func New(start, end address.Address, peer router.PeerName) *Ring { + common.Assert(start < end) + + ring := &Ring{Start: start, End: end, Peer: peer, Entries: make([]*entry, 0)} + ring.updateExportedVariables() + return ring +} + +// TotalRemoteFree returns the approximate number of free IPs +// on other hosts. +func (r *Ring) TotalRemoteFree() address.Offset { + result := address.Offset(0) + for _, entry := range r.Entries { + if entry.Peer != r.Peer { + result += entry.Free + } + } + return result +} + +// Returns the distance between two tokens on this ring, dealing +// with ranges which cross the origin +func (r *Ring) distance(start, end address.Address) address.Offset { + if end > start { + return address.Offset(end - start) + } + + return address.Offset((r.End - start) + (end - r.Start)) +} + +// GrantRangeToHost modifies the ring such that range [start, end) +// is assigned to peer. This may insert up to two new tokens. +// Preconditions: +// - start < end +// - [start, end) must be owned by the calling peer +func (r *Ring) GrantRangeToHost(start, end address.Address, peer router.PeerName) { + //fmt.Printf("%s GrantRangeToHost [%v,%v) -> %s\n", r.Peer, start, end, peer) + + r.assertInvariants() + defer r.assertInvariants() + defer r.updateExportedVariables() + + // ----------------- Start of Checks ----------------- + + common.Assert(start < end) + common.Assert(r.Start <= start && start < r.End) + common.Assert(r.Start < end && end <= r.End) + common.Assert(len(r.Entries) > 0) + + // Look for the left-most entry greater than start, then go one previous + // to get the right-most entry less than or equal to start + preceedingPos := sort.Search(len(r.Entries), func(j int) bool { + return r.Entries[j].Token > start + }) + preceedingPos-- + + // Check all tokens up to end are owned by us + for pos := preceedingPos; pos < len(r.Entries) && r.Entries.entry(pos).Token < end; pos++ { + common.Assert(r.Entries.entry(pos).Peer == r.Peer) + } + + // ----------------- End of Checks ----------------- + + // Free space at start is max(length of range, distance to next token) + startFree := r.distance(start, r.Entries.entry(preceedingPos+1).Token) + if length := r.distance(start, end); startFree > length { + startFree = length + } + // Is there already a token at start, update it + if previousEntry := r.Entries.entry(preceedingPos); previousEntry.Token == start { + previousEntry.update(peer, startFree) + } else { + // Otherwise, these isn't a token here, insert a new one. + r.Entries.insert(entry{Token: start, Peer: peer, Free: startFree}) + preceedingPos++ + // Reset free space on previous entry, which we own. + previousEntry.update(r.Peer, r.distance(previousEntry.Token, start)) + } + + // Give all intervening tokens to the other peer + pos := preceedingPos + 1 + for ; pos < len(r.Entries) && r.Entries.entry(pos).Token < end; pos++ { + entry := r.Entries.entry(pos) + entry.update(peer, entry.Free) + } + + // There is never an entry with a token of r.End, as the end of + // the ring is exclusive. + if end == r.End { + end = r.Start + } + + // If there is a token equal to the end of the range, we don't need to do anything further + if _, found := r.Entries.get(end); found { + return + } + + // If not, we need to insert a token such that we claim this bit on the end. + endFree := r.distance(end, r.Entries.entry(pos).Token) + r.Entries.insert(entry{Token: end, Peer: r.Peer, Free: endFree}) +} + +// Merge the given ring into this ring and return any new ranges added +func (r *Ring) Merge(gossip Ring) error { + r.assertInvariants() + defer r.assertInvariants() + defer r.updateExportedVariables() + + // Don't panic when checking the gossiped in ring. + // In this case just return any error found. + if err := gossip.checkInvariants(); err != nil { + return err + } + + if r.Start != gossip.Start || r.End != gossip.End { + return ErrDifferentSubnets + } + + // Now merge their ring with yours, in a temporary ring. + var result entries + addToResult := func(e entry) { result = append(result, &e) } + + var mine, theirs *entry + var previousOwner *router.PeerName + // i is index into r.Entries; j is index into gossip.Entries + var i, j int + for i < len(r.Entries) && j < len(gossip.Entries) { + mine, theirs = r.Entries[i], gossip.Entries[j] + switch { + case mine.Token < theirs.Token: + addToResult(*mine) + previousOwner = &mine.Peer + i++ + case mine.Token > theirs.Token: + // insert, checking that a range owned by us hasn't been split + if previousOwner != nil && *previousOwner == r.Peer && theirs.Peer != r.Peer { + return ErrEntryInMyRange + } + addToResult(*theirs) + previousOwner = nil + j++ + case mine.Token == theirs.Token: + // merge + switch { + case mine.Version >= theirs.Version: + if mine.Version == theirs.Version && !mine.Equal(theirs) { + common.Debug.Printf("Error merging entries at %s - %v != %v\n", mine.Token, mine, theirs) + return ErrInvalidEntry + } + addToResult(*mine) + previousOwner = &mine.Peer + case mine.Version < theirs.Version: + if mine.Peer == r.Peer { // We shouldn't receive updates to our own tokens + return ErrNewerVersion + } + addToResult(*theirs) + previousOwner = nil + } + i++ + j++ + } + } + + // At this point, either i is at the end of r or j is at the end + // of gossip, so copy over the remaining entries. + + for ; i < len(r.Entries); i++ { + mine = r.Entries[i] + addToResult(*mine) + } + + for ; j < len(gossip.Entries); j++ { + theirs = gossip.Entries[j] + if previousOwner != nil && *previousOwner == r.Peer && theirs.Peer != r.Peer { + return ErrEntryInMyRange + } + addToResult(*theirs) + previousOwner = nil + } + + r.Entries = result + return nil +} + +// Empty returns true if the ring has no entries +func (r *Ring) Empty() bool { + return len(r.Entries) == 0 +} + +// Given a slice of ranges which are all in the right order except +// possibly the last one spans zero, fix that up and return the slice +func (r *Ring) splitRangesOverZero(ranges []address.Range) []address.Range { + if len(ranges) == 0 { + return ranges + } + lastRange := ranges[len(ranges)-1] + // if end token == start (ie last) entry on ring, we want to actually use r.End + if lastRange.End == r.Start { + ranges[len(ranges)-1].End = r.End + } else if lastRange.End <= lastRange.Start { + // We wrapped; want to split around 0 + // First shuffle everything up as we want results to be sorted + ranges = append(ranges, address.Range{}) + copy(ranges[1:], ranges[:len(ranges)-1]) + ranges[0] = address.Range{Start: r.Start, End: lastRange.End} + ranges[len(ranges)-1].End = r.End + } + return ranges +} + +// OwnedRanges returns slice of Ranges, ordered by IP, indicating which +// ranges are owned by this peer. Will split ranges which +// span 0 in the ring. +func (r *Ring) OwnedRanges() (result []address.Range) { + r.assertInvariants() + + for i, entry := range r.Entries { + if entry.Peer == r.Peer { + nextEntry := r.Entries.entry(i + 1) + result = append(result, address.Range{Start: entry.Token, End: nextEntry.Token}) + } + } + + return r.splitRangesOverZero(result) +} + +// ClaimForPeers claims the entire ring for the array of peers passed +// in. Only works for empty rings. +func (r *Ring) ClaimForPeers(peers []router.PeerName) { + common.Assert(r.Empty()) + defer r.assertInvariants() + defer r.updateExportedVariables() + + totalSize := r.distance(r.Start, r.End) + share := totalSize/address.Offset(len(peers)) + 1 + remainder := totalSize % address.Offset(len(peers)) + pos := r.Start + + for i, peer := range peers { + if address.Offset(i) == remainder { + share-- + if share == 0 { + break + } + } + + if e, found := r.Entries.get(pos); found { + e.update(peer, share) + } else { + r.Entries.insert(entry{Token: pos, Peer: peer, Free: share}) + } + + pos += address.Address(share) + } + + common.Assert(pos == r.End) +} + +func (r *Ring) ClaimItAll() { + r.ClaimForPeers([]router.PeerName{r.Peer}) +} + +func (r *Ring) FprintWithNicknames(w io.Writer, m map[router.PeerName]string) { + fmt.Fprintf(w, "Ring [%s, %s)\n", r.Start, r.End) + for _, entry := range r.Entries { + nickname, found := m[entry.Peer] + if found { + nickname = fmt.Sprintf(" (%s)", nickname) + } + + fmt.Fprintf(w, " %s -> %s%s (version: %d, free: %d)\n", entry.Token, + entry.Peer, nickname, entry.Version, entry.Free) + } +} + +func (r *Ring) String() string { + var buffer bytes.Buffer + r.FprintWithNicknames(&buffer, make(map[router.PeerName]string)) + return buffer.String() +} + +// ReportFree is used by the allocator to tell the ring +// how many free ips are in a given range, so that ChoosePeerToAskForSpace +// can make more intelligent decisions. +func (r *Ring) ReportFree(freespace map[address.Address]address.Offset) { + r.assertInvariants() + defer r.assertInvariants() + defer r.updateExportedVariables() + + common.Assert(!r.Empty()) + entries := r.Entries + + // As OwnedRanges splits around the origin, we need to + // detect that here and fix up freespace + if free, found := freespace[r.Start]; found && entries.entry(0).Token != r.Start { + lastToken := entries.entry(-1).Token + prevFree, found := freespace[lastToken] + common.Assert(found) + freespace[lastToken] = prevFree + free + delete(freespace, r.Start) + } + + for start, free := range freespace { + // Look for entry + i := sort.Search(len(entries), func(j int) bool { + return entries[j].Token >= start + }) + + // Are you trying to report free on space I don't own? + common.Assert(i < len(entries) && entries[i].Token == start && entries[i].Peer == r.Peer) + + // Check we're not reporting more space than the range + entry, next := entries.entry(i), entries.entry(i+1) + maxSize := r.distance(entry.Token, next.Token) + common.Assert(free <= maxSize) + + if entries[i].Free == free { + return + } + + entries[i].Free = free + entries[i].Version++ + } +} + +// ChoosePeerToAskForSpace chooses a weighted-random peer to ask +// for space. +func (r *Ring) ChoosePeerToAskForSpace() (result router.PeerName, err error) { + var ( + sum address.Offset + totalSpacePerPeer = make(map[router.PeerName]address.Offset) // Compute total free space per peer + ) + + // iterate through tokens + for _, entry := range r.Entries { + // Ignore ranges with no free space + if entry.Free <= 0 { + continue + } + + // Don't talk to yourself + if entry.Peer == r.Peer { + continue + } + + totalSpacePerPeer[entry.Peer] += entry.Free + sum += entry.Free + } + + if sum == 0 { + err = ErrNoFreeSpace + return + } + + // Pick random peer, weighted by total free space + rn := rand.Int63n(int64(sum)) + for peername, space := range totalSpacePerPeer { + rn -= int64(space) + if rn < 0 { + return peername, nil + } + } + + panic("Should never reach this point") +} + +func (r *Ring) PickPeerForTransfer() router.PeerName { + for _, entry := range r.Entries { + if entry.Peer != r.Peer { + return entry.Peer + } + } + return router.UnknownPeerName +} + +// Transfer will mark all entries associated with 'from' peer as owned by 'to' peer +// and return ranges indicating the new space we picked up +func (r *Ring) Transfer(from, to router.PeerName) (error, []address.Range) { + r.assertInvariants() + defer r.assertInvariants() + defer r.updateExportedVariables() + + var newRanges []address.Range + found := false + + for i, entry := range r.Entries { + if entry.Peer == from { + found = true + entry.Peer = to + entry.Version++ + newRanges = append(newRanges, address.Range{Start: entry.Token, End: r.Entries.entry(i + 1).Token}) + } + } + + if !found { + return ErrNotFound, nil + } + + return nil, r.splitRangesOverZero(newRanges) +} + +// Contains returns true if addr is in this ring +func (r *Ring) Contains(addr address.Address) bool { + return addr >= r.Start && addr < r.End +} + +// Owner returns the peername which owns the range containing addr +func (r *Ring) Owner(token address.Address) router.PeerName { + common.Assert(r.Start <= token && token < r.End) + + r.assertInvariants() + // There can be no owners on an empty ring + if r.Empty() { + return router.UnknownPeerName + } + + // Look for the right-most entry, less than or equal to token + preceedingEntry := sort.Search(len(r.Entries), func(j int) bool { + return r.Entries[j].Token > token + }) + preceedingEntry-- + entry := r.Entries.entry(preceedingEntry) + return entry.Peer +} + +// Get the set of PeerNames mentioned in the ring +func (r *Ring) PeerNames() map[router.PeerName]struct{} { + res := make(map[router.PeerName]struct{}) + + for _, entry := range r.Entries { + res[entry.Peer] = struct{}{} + } + + return res +} diff --git a/ipam/ring/ring_test.go b/ipam/ring/ring_test.go new file mode 100644 index 00000000..1c12c777 --- /dev/null +++ b/ipam/ring/ring_test.go @@ -0,0 +1,713 @@ +package ring + +import ( + "bytes" + "fmt" + "math/rand" + "sort" + "testing" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" + "github.com/weaveworks/weave/router" + wt "github.com/weaveworks/weave/testing" +) + +var ( + peer1name, _ = router.PeerNameFromString("01:00:00:00:02:00") + peer2name, _ = router.PeerNameFromString("02:00:00:00:02:00") + peer3name, _ = router.PeerNameFromString("03:00:00:00:02:00") + + start, end = ParseIP("10.0.0.0"), ParseIP("10.0.0.255") + dot10, dot245 = ParseIP("10.0.0.10"), ParseIP("10.0.0.245") + dot250 = ParseIP("10.0.0.250") + middle = ParseIP("10.0.0.128") +) + +func ParseIP(s string) address.Address { + addr, _ := address.ParseIP(s) + return addr +} + +func TestInvariants(t *testing.T) { + ring := New(start, end, peer1name) + + // Check ring is sorted + ring.Entries = []*entry{{Token: dot245, Peer: peer1name}, {Token: dot10, Peer: peer2name}} + wt.AssertTrue(t, ring.checkInvariants() == ErrNotSorted, "Expected error") + + // Check tokens don't appear twice + ring.Entries = []*entry{{Token: dot245, Peer: peer1name}, {Token: dot245, Peer: peer2name}} + wt.AssertTrue(t, ring.checkInvariants() == ErrTokenRepeated, "Expected error") + + // Check tokens are in bounds + ring = New(dot10, dot245, peer1name) + ring.Entries = []*entry{{Token: start, Peer: peer1name}} + wt.AssertTrue(t, ring.checkInvariants() == ErrTokenOutOfRange, "Expected error") + + ring.Entries = []*entry{{Token: end, Peer: peer1name}} + wt.AssertTrue(t, ring.checkInvariants() == ErrTokenOutOfRange, "Expected error") +} + +func TestInsert(t *testing.T) { + ring := New(start, end, peer1name) + ring.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}} + + wt.AssertPanic(t, func() { + ring.Entries.insert(entry{Token: start, Peer: peer1name}) + }) + + ring.Entries.entry(0).Free = 0 + ring.Entries.insert(entry{Token: dot245, Peer: peer1name}) + ring2 := New(start, end, peer1name) + ring2.Entries = []*entry{{Token: start, Peer: peer1name, Free: 0}, {Token: dot245, Peer: peer1name}} + wt.AssertEquals(t, ring, ring2) + + ring.Entries.insert(entry{Token: dot10, Peer: peer1name}) + ring2.Entries = []*entry{{Token: start, Peer: peer1name, Free: 0}, {Token: dot10, Peer: peer1name}, {Token: dot245, Peer: peer1name}} + wt.AssertEquals(t, ring, ring2) +} + +func TestBetween(t *testing.T) { + ring1 := New(start, end, peer1name) + ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}} + + // First off, in a ring where everything is owned by the peer + // between should return true for everything + for i := 1; i <= 255; i++ { + ip := ParseIP(fmt.Sprintf("10.0.0.%d", i)) + wt.AssertTrue(t, ring1.Entries.between(ip, 0, 1), "between should be true!") + } + + // Now, construct a ring with entries at +10 and -10 + // And check the correct behaviour + + ring1.Entries = []*entry{{Token: dot10, Peer: peer1name}, {Token: dot245, Peer: peer2name}} + ring1.assertInvariants() + for i := 10; i <= 244; i++ { + ipStr := fmt.Sprintf("10.0.0.%d", i) + ip := ParseIP(ipStr) + wt.AssertTrue(t, ring1.Entries.between(ip, 0, 1), + fmt.Sprintf("Between should be true for %s!", ipStr)) + wt.AssertFalse(t, ring1.Entries.between(ip, 1, 2), + fmt.Sprintf("Between should be false for %s!", ipStr)) + } + for i := 0; i <= 9; i++ { + ipStr := fmt.Sprintf("10.0.0.%d", i) + ip := ParseIP(ipStr) + wt.AssertFalse(t, ring1.Entries.between(ip, 0, 1), + fmt.Sprintf("Between should be false for %s!", ipStr)) + wt.AssertTrue(t, ring1.Entries.between(ip, 1, 2), + fmt.Sprintf("Between should be true for %s!", ipStr)) + } + for i := 245; i <= 255; i++ { + ipStr := fmt.Sprintf("10.0.0.%d", i) + ip := ParseIP(ipStr) + wt.AssertFalse(t, ring1.Entries.between(ip, 0, 1), + fmt.Sprintf("Between should be false for %s!", ipStr)) + wt.AssertTrue(t, ring1.Entries.between(ip, 1, 2), + fmt.Sprintf("Between should be true for %s!", ipStr)) + } +} + +func TestGrantSimple(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + // Claim everything for peer1 + ring1.ClaimItAll() + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer1name, Free: 255}}) + + // Now grant everything to peer2 + ring1.GrantRangeToHost(start, end, peer2name) + ring2.Entries = []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}} + wt.AssertEquals(t, ring1.Entries, ring2.Entries) + + // Now spint back to peer 1 + ring2.GrantRangeToHost(dot10, end, peer1name) + ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 10, Version: 2}, + {Token: dot10, Peer: peer1name, Free: 245}} + wt.AssertEquals(t, ring1.Entries, ring2.Entries) + + // And spint back to peer 2 again + ring1.GrantRangeToHost(dot245, end, peer2name) + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer2name, Free: 10, Version: 2}, + {Token: dot10, Peer: peer1name, Free: 235, Version: 1}, + {Token: dot245, Peer: peer2name, Free: 10}}) + + // Grant range spanning a live token + ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 10, Version: 2}, + {Token: dot10, Peer: peer1name, Free: 235}, {Token: dot245, Peer: peer1name, Free: 10}} + ring1.GrantRangeToHost(dot10, end, peer2name) + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer1name, Free: 10, Version: 2}, + {Token: dot10, Peer: peer2name, Free: 235, Version: 1}, + {Token: dot245, Peer: peer2name, Free: 10, Version: 1}}) +} + +func TestGrantSplit(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + // Claim everything for peer1 + ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 255}} + ring2.Merge(*ring1) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) + + // Now grant a split range to peer2 + ring1.GrantRangeToHost(dot10, dot245, peer2name) + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer1name, Free: 10, Version: 1}, + {Token: dot10, Peer: peer2name, Free: 235}, + {Token: dot245, Peer: peer1name, Free: 10}}) +} + +func TestMergeSimple(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + // Claim everything for peer1 + ring1.ClaimItAll() + ring1.GrantRangeToHost(middle, end, peer2name) + wt.AssertSuccess(t, ring2.Merge(*ring1)) + + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer1name, Free: 128, Version: 1}, + {Token: middle, Peer: peer2name, Free: 127}}) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) + + // Now to two different operations on either side, + // check we can Merge again + ring1.GrantRangeToHost(start, middle, peer2name) + ring2.GrantRangeToHost(middle, end, peer1name) + + wt.AssertSuccess(t, ring2.Merge(*ring1)) + wt.AssertSuccess(t, ring1.Merge(*ring2)) + + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer2name, Free: 128, Version: 2}, + {Token: middle, Peer: peer1name, Free: 127, Version: 1}}) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) +} + +func TestMergeErrors(t *testing.T) { + // Cannot Merge in an invalid ring + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + ring2.Entries = []*entry{{Token: middle, Peer: peer2name}, {Token: start, Peer: peer2name}} + wt.AssertTrue(t, ring1.Merge(*ring2) == ErrNotSorted, "Expected ErrNotSorted") + + // Should Merge two rings for different ranges + ring2 = New(start, middle, peer2name) + ring2.Entries = []*entry{} + wt.AssertTrue(t, ring1.Merge(*ring2) == ErrDifferentSubnets, "Expected ErrDifferentSubnets") + + // Cannot Merge newer version of entry I own + ring2 = New(start, end, peer2name) + ring1.Entries = []*entry{{Token: start, Peer: peer1name}} + ring2.Entries = []*entry{{Token: start, Peer: peer1name, Version: 1}} + wt.AssertTrue(t, ring1.Merge(*ring2) == ErrNewerVersion, "Expected ErrNewerVersion") + + // Cannot Merge two entries with same version but different hosts + ring1.Entries = []*entry{{Token: start, Peer: peer1name}} + ring2.Entries = []*entry{{Token: start, Peer: peer2name}} + wt.AssertTrue(t, ring1.Merge(*ring2) == ErrInvalidEntry, "Expected ErrInvalidEntry") + + // Cannot Merge an entry into a range I own + ring1.Entries = []*entry{{Token: start, Peer: peer1name}} + ring2.Entries = []*entry{{Token: middle, Peer: peer2name}} + wt.AssertTrue(t, ring1.Merge(*ring2) == ErrEntryInMyRange, "Expected ErrEntryInMyRange") +} + +func TestMergeMore(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + assertRing := func(ring *Ring, entries entries) { + wt.AssertEquals(t, ring.Entries, entries) + } + + assertRing(ring1, []*entry{}) + assertRing(ring2, []*entry{}) + + // Claim everything for peer1 + ring1.ClaimItAll() + assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}}) + assertRing(ring2, []*entry{}) + + // Check the Merge sends it to the other ring + wt.AssertSuccess(t, ring2.Merge(*ring1)) + assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}}) + assertRing(ring2, []*entry{{Token: start, Peer: peer1name, Free: 255}}) + + // Give everything to peer2 + ring1.GrantRangeToHost(start, end, peer2name) + assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}}) + assertRing(ring2, []*entry{{Token: start, Peer: peer1name, Free: 255}}) + + wt.AssertSuccess(t, ring2.Merge(*ring1)) + assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}}) + assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}}) + + // And carve off some space + ring2.GrantRangeToHost(middle, end, peer1name) + assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2}, + {Token: middle, Peer: peer1name, Free: 127}}) + assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 255, Version: 1}}) + + // And Merge back + wt.AssertSuccess(t, ring1.Merge(*ring2)) + assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2}, + {Token: middle, Peer: peer1name, Free: 127}}) + assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2}, + {Token: middle, Peer: peer1name, Free: 127}}) + + // This should be a no-op + wt.AssertSuccess(t, ring2.Merge(*ring1)) + assertRing(ring1, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2}, + {Token: middle, Peer: peer1name, Free: 127}}) + assertRing(ring2, []*entry{{Token: start, Peer: peer2name, Free: 128, Version: 2}, + {Token: middle, Peer: peer1name, Free: 127}}) +} + +func TestMergeSplit(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + // Claim everything for peer2 + ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 255}} + wt.AssertSuccess(t, ring2.Merge(*ring1)) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) + + // Now grant a split range to peer1 + ring2.GrantRangeToHost(dot10, dot245, peer1name) + wt.AssertEquals(t, ring2.Entries, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1}, + {Token: dot10, Peer: peer1name, Free: 235}, + {Token: dot245, Peer: peer2name, Free: 10}}) + wt.AssertSuccess(t, ring1.Merge(*ring2)) + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1}, + {Token: dot10, Peer: peer1name, Free: 235}, + {Token: dot245, Peer: peer2name, Free: 10}}) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) +} + +func TestMergeSplit2(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + // Claim everything for peer2 + ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 250}, {Token: dot250, Peer: peer2name, Free: 5}} + wt.AssertSuccess(t, ring2.Merge(*ring1)) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) + + // Now grant a split range to peer1 + ring2.GrantRangeToHost(dot10, dot245, peer1name) + wt.AssertEquals(t, ring2.Entries, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1}, + {Token: dot10, Peer: peer1name, Free: 235}, + {Token: dot245, Peer: peer2name, Free: 5}, {Token: dot250, Peer: peer2name, Free: 5}}) + wt.AssertSuccess(t, ring1.Merge(*ring2)) + wt.AssertEquals(t, ring1.Entries, entries{{Token: start, Peer: peer2name, Free: 10, Version: 1}, + {Token: dot10, Peer: peer1name, Free: 235}, + {Token: dot245, Peer: peer2name, Free: 5}, {Token: dot250, Peer: peer2name, Free: 5}}) + wt.AssertEquals(t, ring1.Entries, ring2.Entries) +} + +// 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) + + assertRing := func(ring *Ring, entries entries) { + wt.AssertEquals(t, ring.Entries, entries) + } + + assertRing(ring1, []*entry{}) + assertRing(ring2, []*entry{}) + + // Claim everything for peer1 + ring1.ClaimItAll() + assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}}) + assertRing(ring2, []*entry{}) + + // Check the Merge sends it to the other ring + wt.AssertSuccess(t, ring2.Merge(*ring1)) + assertRing(ring1, []*entry{{Token: start, Peer: peer1name, Free: 255}}) + assertRing(ring2, []*entry{{Token: start, Peer: peer1name, Free: 255}}) +} + +func TestFindFree(t *testing.T) { + ring1 := New(start, end, peer1name) + + _, err := ring1.ChoosePeerToAskForSpace() + wt.AssertTrue(t, err == ErrNoFreeSpace, "Expected ErrNoFreeSpace") + + ring1.Entries = []*entry{{Token: start, Peer: peer1name}} + _, err = ring1.ChoosePeerToAskForSpace() + wt.AssertTrue(t, err == ErrNoFreeSpace, "Expected ErrNoFreeSpace") + + // We shouldn't return outselves + ring1.ReportFree(map[address.Address]address.Offset{start: 10}) + _, err = ring1.ChoosePeerToAskForSpace() + wt.AssertTrue(t, err == ErrNoFreeSpace, "Expected ErrNoFreeSpace") + + ring1.Entries = []*entry{{Token: start, Peer: peer1name, Free: 1}, + {Token: start, Peer: peer1name, Free: 1}} + _, err = ring1.ChoosePeerToAskForSpace() + wt.AssertTrue(t, err == ErrNoFreeSpace, "Expected ErrNoFreeSpace") + + // We should return others + ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 1}} + peer, err := ring1.ChoosePeerToAskForSpace() + wt.AssertSuccess(t, err) + wt.AssertEquals(t, peer, peer2name) + + ring1.Entries = []*entry{{Token: start, Peer: peer2name, Free: 1}, + {Token: start, Peer: peer2name, Free: 1}} + peer, err = ring1.ChoosePeerToAskForSpace() + wt.AssertSuccess(t, err) + wt.AssertEquals(t, peer, peer2name) +} + +func TestReportFree(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + ring1.ClaimItAll() + ring1.GrantRangeToHost(middle, end, peer2name) + wt.AssertSuccess(t, ring2.Merge(*ring1)) + + freespace := make(map[address.Address]address.Offset) + for _, r := range ring2.OwnedRanges() { + freespace[r.Start] = 0 + } + ring2.ReportFree(freespace) +} + +func TestMisc(t *testing.T) { + ring := New(start, end, peer1name) + + wt.AssertTrue(t, ring.Empty(), "empty") + + ring.ClaimItAll() + println(ring.String()) +} + +func TestEmptyGossip(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + ring1.ClaimItAll() + // This used to panic, and it shouldn't + wt.AssertSuccess(t, ring1.Merge(*ring2)) +} + +func TestMergeOldMessage(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + ring1.ClaimItAll() + wt.AssertSuccess(t, ring2.Merge(*ring1)) + + ring1.GrantRangeToHost(middle, end, peer1name) + wt.AssertSuccess(t, ring1.Merge(*ring2)) +} + +func TestSplitRangeAtBeginning(t *testing.T) { + ring1 := New(start, end, peer1name) + ring2 := New(start, end, peer2name) + + ring1.ClaimItAll() + wt.AssertSuccess(t, ring2.Merge(*ring1)) + + ring1.GrantRangeToHost(start, middle, peer2name) + wt.AssertSuccess(t, ring2.Merge(*ring1)) +} + +func TestOwnedRange(t *testing.T) { + ring1 := New(start, end, peer1name) + ring1.ClaimItAll() + + wt.AssertEquals(t, ring1.OwnedRanges(), []address.Range{{Start: start, End: end}}) + + ring1.GrantRangeToHost(middle, end, peer2name) + wt.AssertEquals(t, ring1.OwnedRanges(), []address.Range{{Start: start, End: middle}}) + + ring2 := New(start, end, peer2name) + ring2.Merge(*ring1) + wt.AssertEquals(t, ring2.OwnedRanges(), []address.Range{{Start: middle, End: end}}) + + ring2.Entries = []*entry{{Token: middle, Peer: peer2name}} + wt.AssertEquals(t, ring2.OwnedRanges(), + []address.Range{{Start: start, End: middle}, {Start: middle, End: end}}) + + ring2.Entries = []*entry{{Token: dot10, Peer: peer2name}, {Token: middle, Peer: peer2name}} + wt.AssertEquals(t, ring2.OwnedRanges(), + []address.Range{{Start: start, End: dot10}, {Start: dot10, End: middle}, + {Start: middle, End: end}}) +} + +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.ClaimItAll() + ring1.GrantRangeToHost(middle, end, peer2name) + ring1.Transfer(peer2name, peer1name) + wt.AssertEquals(t, ring1.OwnedRanges(), []address.Range{{start, middle}, {middle, end}}) + + // 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.ClaimItAll() + ring1.GrantRangeToHost(middle, end, peer2name) + ring1.Transfer(peer2name, peer1name) + ring1.GrantRangeToHost(dot10, middle, peer2name) + wt.AssertEquals(t, ring1.OwnedRanges(), []address.Range{{start, dot10}, {middle, end}}) +} + +func TestOwner(t *testing.T) { + ring1 := New(start, end, peer1name) + wt.AssertTrue(t, ring1.Contains(start), "start should be in ring") + wt.AssertFalse(t, ring1.Contains(end), "end should not be in ring") + + wt.AssertEquals(t, ring1.Owner(start), router.UnknownPeerName) + + ring1.ClaimItAll() + ring1.GrantRangeToHost(middle, end, peer2name) + wt.AssertEquals(t, ring1.Owner(start), peer1name) + wt.AssertEquals(t, ring1.Owner(middle), peer2name) + wt.AssertPanic(t, func() { + ring1.Owner(end) + }) +} + +type addressSlice []address.Address + +func (s addressSlice) Len() int { return len(s) } +func (s addressSlice) Less(i, j int) bool { return s[i] < s[j] } +func (s addressSlice) Swap(i, j int) { s[i], s[j] = s[j], s[i] } + +func TestFuzzRing(t *testing.T) { + var ( + numPeers = 25 + iterations = 1000 + ) + + peers := make([]router.PeerName, numPeers) + for i := 0; i < numPeers; i++ { + peer, _ := router.PeerNameFromString(fmt.Sprintf("%02d:00:00:00:02:00", i)) + peers[i] = peer + } + + // Make a valid, random ring + makeGoodRandomRing := func() *Ring { + addressSpace := end - start + numTokens := rand.Intn(int(addressSpace)) + + tokenMap := make(map[address.Address]bool) + for i := 0; i < numTokens; i++ { + tokenMap[address.Address(rand.Intn(int(addressSpace)))] = true + } + var tokens []address.Address + for token := range tokenMap { + tokens = append(tokens, token) + } + sort.Sort(addressSlice(tokens)) + + peer := peers[rand.Intn(len(peers))] + ring := New(start, end, peer) + for _, token := range tokens { + peer = peers[rand.Intn(len(peers))] + ring.Entries = append(ring.Entries, &entry{Token: start + token, Peer: peer}) + } + + ring.assertInvariants() + return ring + } + + for i := 0; i < iterations; i++ { + // make 2 random rings + ring1 := makeGoodRandomRing() + ring2 := makeGoodRandomRing() + + // Merge them - this might fail, we don't care + // We just want to make sure it doesn't panic + ring1.Merge(*ring2) + + // Check whats left still passes assertions + ring1.assertInvariants() + ring2.assertInvariants() + } + + // Make an invalid, random ring + makeBadRandomRing := func() *Ring { + addressSpace := end - start + numTokens := rand.Intn(int(addressSpace)) + tokens := make([]address.Address, numTokens) + for i := 0; i < numTokens; i++ { + tokens[i] = address.Address(rand.Intn(int(addressSpace))) + } + + peer := peers[rand.Intn(len(peers))] + ring := New(start, end, peer) + for _, token := range tokens { + peer = peers[rand.Intn(len(peers))] + ring.Entries = append(ring.Entries, &entry{Token: start + token, Peer: peer}) + } + + return ring + } + + for i := 0; i < iterations; i++ { + // make 2 random rings + ring1 := makeGoodRandomRing() + ring2 := makeBadRandomRing() + + // Merge them - this might fail, we don't care + // We just want to make sure it doesn't panic + ring1.Merge(*ring2) + + // Check whats left still passes assertions + ring1.assertInvariants() + } +} + +func TestFuzzRingHard(t *testing.T) { + //common.InitDefaultLogging(true) + var ( + numPeers = 100 + iterations = 3000 + peers []router.PeerName + rings []*Ring + nextPeerID = 0 + ) + + addPeer := func() { + peer, _ := router.PeerNameFromString(fmt.Sprintf("%02d:%02d:00:00:00:00", nextPeerID/10, nextPeerID%10)) + common.Debug.Printf("%s: Adding peer", peer) + nextPeerID++ + peers = append(peers, peer) + rings = append(rings, New(start, end, peer)) + } + + for i := 0; i < numPeers; i++ { + addPeer() + } + + rings[0].ClaimItAll() + + randomPeer := func(exclude int) (int, router.PeerName, *Ring) { + var peerIndex int + if exclude >= 0 { + peerIndex = rand.Intn(len(peers) - 1) + if peerIndex == exclude { + peerIndex++ + } + } else { + peerIndex = rand.Intn(len(peers)) + } + return peerIndex, peers[peerIndex], rings[peerIndex] + } + + // Keep a map of index -> ranges, as these are a little expensive to + // calculate for every ring on every iteration. + var theRanges = make(map[int][]address.Range) + theRanges[0] = rings[0].OwnedRanges() + + addOrRmPeer := func() { + if len(peers) < numPeers { + addPeer() + return + } + + peerIndex, peername, _ := randomPeer(-1) + // Remove peer from our state + peers = append(peers[:peerIndex], peers[peerIndex+1:]...) + rings = append(rings[:peerIndex], rings[peerIndex+1:]...) + theRanges = make(map[int][]address.Range) + + // Transfer the space for this peer on another peer, but not this one + _, otherPeername, otherRing := randomPeer(peerIndex) + + // We need to be in a ~converged ring to rmpeer + for _, ring := range rings { + wt.AssertSuccess(t, otherRing.Merge(*ring)) + } + + common.Debug.Printf("%s: transferring from peer %s", otherPeername, peername) + otherRing.Transfer(peername, peername) + + // And now tell everyone about the transfer - rmpeer is + // not partition safe + for i, ring := range rings { + wt.AssertSuccess(t, ring.Merge(*otherRing)) + theRanges[i] = ring.OwnedRanges() + } + } + + doGrantOrGossip := func() { + var ringsWithRanges = make([]int, 0, len(rings)) + for index, ranges := range theRanges { + if len(ranges) > 0 { + ringsWithRanges = append(ringsWithRanges, index) + } + } + + if len(ringsWithRanges) > 0 { + // Produce a random split in a random owned range, given to a random peer + indexWithRanges := ringsWithRanges[rand.Intn(len(ringsWithRanges))] + ownedRanges := theRanges[indexWithRanges] + ring := rings[indexWithRanges] + + rangeToSplit := ownedRanges[rand.Intn(len(ownedRanges))] + size := address.Subtract(rangeToSplit.End, rangeToSplit.Start) + ipInRange := address.Add(rangeToSplit.Start, address.Offset(rand.Intn(int(size)))) + _, peerToGiveTo, _ := randomPeer(-1) + common.Debug.Printf("%s: Granting [%v, %v) to %s", ring.Peer, ipInRange, rangeToSplit.End, peerToGiveTo) + ring.GrantRangeToHost(ipInRange, rangeToSplit.End, peerToGiveTo) + + // Now 'gossip' this to a random host (note, note could be same host as above) + otherIndex, _, otherRing := randomPeer(-1) + common.Debug.Printf("%s: 'Gossiping' to %s", ring.Peer, otherRing.Peer) + wt.AssertSuccess(t, otherRing.Merge(*ring)) + + theRanges[indexWithRanges] = ring.OwnedRanges() + theRanges[otherIndex] = otherRing.OwnedRanges() + return + } + + // No rings think they own anything (as gossip might be behind) + // We're going to pick a random host (which has entries) and gossip + // it to a random host (which may or may not have entries). + var ringsWithEntries = make([]*Ring, 0, len(rings)) + for _, ring := range rings { + if len(ring.Entries) > 0 { + ringsWithEntries = append(ringsWithEntries, ring) + } + } + ring1 := ringsWithEntries[rand.Intn(len(ringsWithEntries))] + ring2index, _, ring2 := randomPeer(-1) + common.Debug.Printf("%s: 'Gossiping' to %s", ring1.Peer, ring2.Peer) + wt.AssertSuccess(t, ring2.Merge(*ring1)) + theRanges[ring2index] = ring2.OwnedRanges() + } + + for i := 0; i < iterations; i++ { + // about 1 in 10 times, rmpeer or add host + n := rand.Intn(10) + switch { + case n < 1: + addOrRmPeer() + default: + doGrantOrGossip() + } + } +} + +func (es entries) String() string { + var buffer bytes.Buffer + fmt.Fprintf(&buffer, "[") + for i, entry := range es { + fmt.Fprintf(&buffer, "%+v", *entry) + if i+1 < len(es) { + fmt.Fprintf(&buffer, " ") + } + } + fmt.Fprintf(&buffer, "]") + return buffer.String() +} diff --git a/ipam/utils/utils.go b/ipam/utils/utils.go new file mode 100644 index 00000000..18957155 --- /dev/null +++ b/ipam/utils/utils.go @@ -0,0 +1,49 @@ +package utils + +import ( + "net" +) + +// We shouldn't ever get any errors on *encoding*, but if we do, this will make sure we get to hear about them. +func panicOnError(err error) { + if err != nil { + panic(err) + } +} + +// IP4int converts an ipv4 address to a uint32 +func IP4int(ip4 net.IP) (r uint32) { + for _, b := range ip4.To4() { + r <<= 8 + r |= uint32(b) + } + return +} + +// IntIP4 converts a uint32 to an ipv4 address +func IntIP4(key uint32) (r net.IP) { + r = make([]byte, net.IPv4len) + for i := 3; i >= 0; i-- { + r[i] = byte(key) + key >>= 8 + } + return +} + +// Add - convert to 32-bit unsigned integer, add, and convert back +func Add(addr net.IP, i uint32) net.IP { + sum := IP4int(addr) + i + return IntIP4(sum) +} + +// Subtract - convert to 32-bit unsigned integer, subtract, and convert back +func Subtract(a, b net.IP) int64 { + return int64(IP4int(a)) - int64(IP4int(b)) +} + +// Assert test is true, panic otherwise +func Assert(test bool, message string) { + if !test { + panic(message) + } +} diff --git a/testing/util.go b/testing/util.go index fa982dc4..c637eff5 100644 --- a/testing/util.go +++ b/testing/util.go @@ -40,12 +40,29 @@ func AssertNoErr(t *testing.T, err error) { } } +func AssertBool(t *testing.T, got, wanted bool, desc string) { + if got != wanted { + Fatalf(t, "Expected %s %t but got %t", desc, wanted, got) + } +} + +func AssertEqualUint32(t *testing.T, got, wanted uint32, desc string) { + if got != wanted { + Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) + } +} + func AssertEqualuint64(t *testing.T, got, wanted uint64, desc string) { if got != wanted { Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) } } +func AssertEqualInt64(t *testing.T, got, wanted int64, desc string) { + if got != wanted { + Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) + } +} func AssertEqualInt(t *testing.T, got, wanted int, desc string) { if got != wanted { Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) @@ -64,6 +81,18 @@ func AssertEqualString(t *testing.T, got, wanted string, desc string) { } } +func AssertNotEqualString(t *testing.T, got, wanted string, desc string) { + if got == wanted { + Fatalf(t, "Expected %s unlike '%s'", desc, wanted) + } +} + +func AssertEquals(t *testing.T, a1, a2 interface{}) { + if !reflect.DeepEqual(a1, a2) { + Fatalf(t, "Expected %+v == %+v", a1, a2) + } +} + func AssertStatus(t *testing.T, got int, wanted int, desc string) { if got != wanted { Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) @@ -72,6 +101,9 @@ func AssertStatus(t *testing.T, got int, wanted int, desc string) { func AssertErrorInterface(t *testing.T, got interface{}, wanted interface{}, desc string) { gotT, wantedT := reflect.TypeOf(got), reflect.TypeOf(wanted).Elem() + if got == nil { + Fatalf(t, "Expected %s but got nil (%s)", wantedT.String(), desc) + } if !gotT.Implements(wantedT) { Fatalf(t, "Expected %s but got %s (%s)", wantedT.String(), gotT.String(), desc) } @@ -79,6 +111,9 @@ func AssertErrorInterface(t *testing.T, got interface{}, wanted interface{}, des func AssertErrorType(t *testing.T, got interface{}, wanted interface{}, desc string) { gotT, wantedT := reflect.TypeOf(got), reflect.TypeOf(wanted).Elem() + if got == nil { + Fatalf(t, "Expected %s but got nil (%s)", wantedT.String(), desc) + } if gotT != wantedT { Fatalf(t, "Expected %s but got %s (%s)", wantedT.String(), gotT.String(), desc) } @@ -97,6 +132,26 @@ func AssertEmpty(t *testing.T, array interface{}, desc string) { } } +func AssertSuccess(t *testing.T, err error) { + if err != nil { + Fatalf(t, "Expected success, got '%s'", err.Error()) + } +} + +func AssertPanic(t *testing.T, f func()) { + wrapper := func() (paniced bool) { + defer func() { + if err := recover(); err != nil { + paniced = true + } + }() + + f() + return + } + AssertTrue(t, wrapper(), "Expected panic") +} + // Like testing.Fatalf, but adds the stack trace of the current call func Fatalf(t *testing.T, format string, args ...interface{}) { t.Fatalf(format+"\n%s", append(args, StackTrace())...) From 72112d7aef86e89a370d0720e4fef8d6c592b394 Mon Sep 17 00:00:00 2001 From: David Wragg Date: Thu, 14 May 2015 17:24:19 +0100 Subject: [PATCH 2/7] Implementation of Basic Paxos --- ipam/paxos/paxos.go | 261 +++++++++++++++++++++++++++ ipam/paxos/paxos_test.go | 372 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 633 insertions(+) create mode 100644 ipam/paxos/paxos.go create mode 100644 ipam/paxos/paxos_test.go diff --git a/ipam/paxos/paxos.go b/ipam/paxos/paxos.go new file mode 100644 index 00000000..fd67283d --- /dev/null +++ b/ipam/paxos/paxos.go @@ -0,0 +1,261 @@ +package paxos + +import ( + "fmt" + "github.com/weaveworks/weave/router" +) + +// The node identifier. The use of the UID here is important: Paxos +// acceptors must not forget their promises, so it's important that a +// node does not restart and lose its Paxos state but claim to have +// the same ID. +type NodeID struct { + Name router.PeerName + UID router.PeerUID +} + +// note all fields exported in structs so we can Gob them +type ProposalID struct { + // round numbers begin at 1. round 0 indicates an + // uninitialized ProposalID, and precedes all other ProposalIDs + Round uint + Proposer NodeID +} + +func (a ProposalID) precedes(b ProposalID) bool { + switch { + case a.Round != b.Round: + return a.Round < b.Round + case a.Proposer.Name != b.Proposer.Name: + return a.Proposer.Name < b.Proposer.Name + case a.Proposer.UID != b.Proposer.UID: + return a.Proposer.UID < b.Proposer.UID + default: + return false + } +} + +func (a ProposalID) valid() bool { + return a.Round > 0 +} + +// For seeding IPAM, the value we want consensus on is a set of peer names +type Value []router.PeerName + +// An AcceptedValue is a Value plus the proposal which originated that +// Value. The origin is not essential, but makes comparing +// AcceptedValues easy even if comparing Values is not. +type AcceptedValue struct { + Value Value + Origin ProposalID +} + +type NodeClaims struct { + // The node promises not to accept a proposal with id less + // than this. + Promise ProposalID + + // The accepted proposal, if valid + Accepted ProposalID + AcceptedVal AcceptedValue +} + +func (a NodeClaims) equals(b NodeClaims) bool { + return a.Promise == b.Promise && a.Accepted == b.Accepted && + a.AcceptedVal.Origin == b.AcceptedVal.Origin +} + +type GossipState map[NodeID]NodeClaims + +type Node struct { + id NodeID + quorum uint + knows GossipState +} + +func NewNode(name router.PeerName, uid router.PeerUID, quorum uint) *Node { + return &Node{ + id: NodeID{name, uid}, + quorum: quorum, + knows: map[NodeID]NodeClaims{}, + } +} + +func (node *Node) GossipState() GossipState { + return node.knows +} + +// Update this node's information about what other nodes know. +// Returns true if we learned something new. +func (node *Node) Update(from GossipState) bool { + changed := false + + for i, fromClaims := range from { + claims, ok := node.knows[i] + if ok { + if claims.Promise.precedes(fromClaims.Promise) { + claims.Promise = fromClaims.Promise + changed = true + } + + if claims.Accepted.precedes(fromClaims.Accepted) { + claims.Accepted = fromClaims.Accepted + claims.AcceptedVal = fromClaims.AcceptedVal + changed = true + } + } else { + claims = fromClaims + changed = true + } + + node.knows[i] = claims + } + + return changed +} + +func max(a uint, b uint) uint { + if a > b { + return a + } else { + return b + } +} + +// Initiate a new proposal, i.e. the Paxos "Prepare" step. This is +// simply a matter of gossipping a new proposal that supersedes all +// others. +func (node *Node) Propose() { + // Find the highest round number around + round := uint(0) + + for _, claims := range node.knows { + round = max(round, claims.Promise.Round) + round = max(round, claims.Accepted.Round) + } + + ourClaims := node.knows[node.id] + ourClaims.Promise = ProposalID{ + Round: round + 1, + Proposer: node.id, + } + node.knows[node.id] = ourClaims + + // With a quorum of 1, we can immediately accept our proposal + if node.quorum == 1 { + node.Think() + } +} + +// The heart of the consensus algorithm. Return true if we have +// changed our claims. +func (node *Node) Think() bool { + ourClaims := node.knows[node.id] + + // The "Promise" step of Paxos: Copy the highest known + // promise. + for _, claims := range node.knows { + if ourClaims.Promise.precedes(claims.Promise) { + ourClaims.Promise = claims.Promise + } + } + + // The "Accept Request" step of Paxos: Acting as a proposer, + // do we have a proposal that has been promised by a quorum? + // + // In Paxos, the "proposer" and "acceptor" roles are distinct, + // so in principle a node acting as a proposer could continue + // trying to get its proposal acccepted even after the same + // node as an acceptor has superseded that proposal. But + // that's pointless in a gossip context: If our promise + // supersedes our own proposal, then anyone who hears about + // that promise will not accept that proposal. So our + // proposal is only in the running if it is also our promise. + if ourClaims.Promise.Proposer == node.id { + // Determine whether a quorum has promised, and the + // best previously-accepted value if there is one. + count := uint(0) + var accepted ProposalID + var acceptedVal AcceptedValue + + for _, claims := range node.knows { + if claims.Promise == ourClaims.Promise { + count++ + + if accepted.precedes(claims.Accepted) { + accepted = claims.Accepted + acceptedVal = claims.AcceptedVal + } + } + } + + if count >= node.quorum { + if !accepted.valid() { + acceptedVal.Value = node.pickValue() + acceptedVal.Origin = ourClaims.Promise + } + + // We automatically accept our own proposal, + // and that's how we communicate the "accept + // request" to other nodes. + ourClaims.Accepted = ourClaims.Promise + ourClaims.AcceptedVal = acceptedVal + } + } + + // The "Accepted" step of Paxos: If the proposal we promised + // on got accepted by some other node, we accept it too. + for _, claims := range node.knows { + if claims.Accepted == ourClaims.Promise { + ourClaims.Accepted = claims.Accepted + ourClaims.AcceptedVal = claims.AcceptedVal + break + } + } + + if ourClaims.equals(node.knows[node.id]) { + return false + } + + node.knows[node.id] = ourClaims + return true +} + +// When we get to pick a value, we use the set of peer names we know +// about. This is not necessarily all peer names, but it is at least +// a quorum, and so good enough for seeding the ring. +func (node *Node) pickValue() Value { + val := make([]router.PeerName, len(node.knows)) + i := 0 + for id := range node.knows { + val[i] = id.Name + i++ + } + return val +} + +// Has a consensus been reached, based on the known claims of other nodes? +func (node *Node) Consensus() (bool, AcceptedValue) { + counts := map[ProposalID]uint{} + + for _, claims := range node.knows { + if claims.Accepted.valid() { + origin := claims.AcceptedVal.Origin + count := counts[origin] + 1 + counts[origin] = count + if count >= node.quorum { + return true, claims.AcceptedVal + } + } + } + + return false, AcceptedValue{} +} + +func (node *Node) String() string { + if ok, val := node.Consensus(); ok { + return fmt.Sprintf("Consensus reached with size %d", len(val.Value)) + } else { + return fmt.Sprintf("Nodes known: %d, Quorum size: %d", len(node.knows), node.quorum) + } +} diff --git a/ipam/paxos/paxos_test.go b/ipam/paxos/paxos_test.go new file mode 100644 index 00000000..cd704c5c --- /dev/null +++ b/ipam/paxos/paxos_test.go @@ -0,0 +1,372 @@ +package paxos + +import ( + "fmt" + "github.com/weaveworks/weave/router" + "math/rand" + "testing" + "time" +) + +type TestNode struct { + *Node + + // Topology + links []*Link + isolated bool + + // The first consensus the Node observed + firstConsensus AcceptedValue +} + +type Link struct { + from *TestNode + to *TestNode + converse *Link + + // A link is considered ready if it is worthwhile gossipping + // along it (i.e. unless we already gossipped along it and the + // "from" node didn't change since then). If this is <0, then + // the link is not ready. Otherwise, this gives the index in + // the Model's readyLinks slice. + ready int +} + +type Model struct { + t *testing.T + r *rand.Rand + quorum uint + nodes []TestNode + readyLinks []*Link + nextID uint +} + +func (m *Model) addLink(a, b *TestNode) { + if a == b { + // Never link a node to itself + return + } + + ab := Link{from: a, to: b, ready: len(m.readyLinks)} + ba := Link{from: b, to: a, converse: &ab, ready: len(m.readyLinks) + 1} + ab.converse = &ba + a.links = append(a.links, &ab) + b.links = append(b.links, &ba) + m.readyLinks = append(m.readyLinks, &ab, &ba) +} + +func (m *Model) linkExists(a, b *TestNode) bool { + for _, l := range a.links { + if l.to == b { + return true + } + } + + return false +} + +type TestParams struct { + // Number of nodes + nodeCount uint + + // Probability that two nodes are connected. + connectedProb float32 + + // Probability that some node will re-propose at each + // step. Setting this too high makes it likely that we'll fail + // to converge. + reproposeProb float32 + + // Probability that some node will be isolated at each step. + isolateProb float32 + + // Probability that some node willbe restarted at each step. + restartProb float32 +} + +// Make a network of nodes with random topology +func makeRandomModel(params *TestParams, r *rand.Rand, t *testing.T) *Model { + m := Model{ + t: t, + r: r, + quorum: params.nodeCount/2 + 1, + nodes: make([]TestNode, params.nodeCount), + readyLinks: []*Link{}, + nextID: params.nodeCount + 1, + } + + for i := range m.nodes { + m.nodes[i].Node = NewNode(router.PeerName(i/2+1), + router.PeerUID(r.Int63()), m.quorum) + m.nodes[i].Propose() + } + + for i := 1; i < len(m.nodes); i++ { + // was node i connected to the other nodes yet? + connected := false + + for j := 0; j < i; j++ { + if r.Float32() < params.connectedProb { + connected = true + m.addLink(&m.nodes[i], &m.nodes[j]) + } + } + + if !connected { + // node i must be connected into the graph + // somewhere. So if we didn't connect it + // already, this is a last resort. + m.addLink(&m.nodes[i], &m.nodes[r.Intn(i)]) + } + } + + return &m +} + +// Mark a link as unready +func (m *Model) unreadyLink(link *Link) { + i := link.ready + if i >= 0 { + m.readyLinks[i] = m.readyLinks[len(m.readyLinks)-1] + m.readyLinks[i].ready = i + m.readyLinks = m.readyLinks[:len(m.readyLinks)-1] + link.ready = -1 + } +} + +// Mark a link as ready +func (m *Model) readyLink(link *Link) { + if link.ready < 0 { + link.ready = len(m.readyLinks) + m.readyLinks = append(m.readyLinks, link) + } +} + +// Mark all the outgoing links from a node as ready +func (m *Model) nodeChanged(node *TestNode) { + for _, l := range node.links { + m.readyLink(l) + } +} + +// Isolate a node +func (m *Model) isolateNode(node *TestNode) { + node.isolated = true + for _, l := range node.links { + if l.ready >= 0 { + m.unreadyLink(l) + m.unreadyLink(l.converse) + } + } + + // Isolating a node could partition the network. We don't + // want to test such a case (because it could prevent + // consensus). Checking for partitions would be more code + // than its worth, so just add some links to prevent the + // possibility of partitions. + for i := 1; i < len(node.links); i++ { + m.addLink(node.links[i-1].to, node.links[i].to) + } +} + +// Restart a node +func (m *Model) restart(node *TestNode) { + node.Node = NewNode(router.PeerName(m.nextID), + router.PeerUID(m.r.Int63()), m.quorum) + m.nextID++ + node.Propose() + + // The node is now ignorant, so we need to mark the links into + // the node as ready. + for _, l := range node.links { + if !l.to.isolated { + m.readyLink(l.converse) + } + } + + // If a consensus was just accepted due to this node accepting + // it, without other nodes hearing of it, and we then restart + // this node, then a different consensus can occur later on. + // If a tree falls with no one to hear it, does it make a + // sound? + node.firstConsensus = AcceptedValue{} +} + +func (m *Model) pickNode() *TestNode { + for { + node := &m.nodes[m.r.Intn(len(m.nodes))] + if !node.isolated { + return node + } + } +} + +func (m *Model) simulate(params *TestParams) bool { + nodesLeft := uint(len(m.nodes)) + restarts := uint(0) + + for step := 0; step < 1000000; step++ { + if len(m.readyLinks) == 0 { + // Everything has converged. This can be + // because consensus was reached, or because + // consensus became impossible, e.g. everyone + // promised on a particular proposal, but then + // the proposer restarted or was isolated. So + // we detect the latter cases and force a new + // proposal + for i := range m.nodes { + ok, _ := m.nodes[i].Consensus() + if ok { + return true + } + } + + node := m.pickNode() + node.Propose() + m.nodeChanged(node) + } + + // Pick a ready link at random + i := m.r.Intn(len(m.readyLinks)) + link := m.readyLinks[i] + if link.ready != i { + m.t.Fatal("Link in readyLinks was not ready") + } + + // gossip across link + node := link.to + if node.Update(link.from.GossipState()) { + node.Think() + + if !node.firstConsensus.Origin.valid() { + ok, val := node.Consensus() + if ok { + node.firstConsensus = val + } + } + + m.nodeChanged(node) + } + + m.unreadyLink(link) + + // Re-propose? + if m.r.Float32() < params.reproposeProb { + node := m.pickNode() + node.Propose() + m.nodeChanged(node) + } + + // Isolate? + if nodesLeft > m.quorum && m.r.Float32() < params.isolateProb { + m.isolateNode(m.pickNode()) + nodesLeft-- + + // We isolated a node, so get another node to + // re-propose. In reality the lack of + // consensus would be detected via a timeout + node := m.pickNode() + node.Propose() + m.nodeChanged(node) + } + + // Restart? + if restarts < m.quorum && m.r.Float32() < params.restartProb { + restarts++ + node := m.pickNode() + m.restart(node) + m.nodeChanged(node) + } + } + + return false +} + +func (m *Model) dump() { + for i := range m.nodes { + node := &m.nodes[i] + fmt.Println(node.id) + for n, claims := range node.knows { + fmt.Printf("\t%d %v\n", n, claims) + } + } +} + +// Validate the final model state +func (m *Model) validate() { + var origin ProposalID + + for i := range m.nodes { + ok, val := m.nodes[i].Consensus() + if !ok { + //m.dump() + m.t.Fatal("Node doesn't know about consensus") + } + + firstConsensus := m.nodes[i].firstConsensus + if firstConsensus.Origin.valid() && + firstConsensus.Origin != val.Origin { + //m.dump() + m.t.Fatal("Consensus mismatch") + } + + if i == 0 { + origin = val.Origin + } else if val.Origin != origin { + m.t.Fatal("Node disagrees about consensus") + } + } +} + +func TestSingleNode(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + // Test the single node case + params := &TestParams{ + nodeCount: 1, + connectedProb: 0, + reproposeProb: 0, + isolateProb: 0, + restartProb: 0, + } + + m := makeRandomModel(params, r, t) + + if !m.simulate(params) { + m.t.Fatal("Failed to converge") + } + + m.validate() +} + +func TestPaxos(t *testing.T) { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + + params := &TestParams{ + nodeCount: 10, + connectedProb: 0.5, + reproposeProb: 0.01, + isolateProb: 0.01, + + // Restarts cause failures due to the case where 1) + // the gossip ordering is such that there is an + // effective partition from the start, with a quorum + // on one side and one node less than a quorum on the + // other; 2) the quorum reaches a consensus; 3) a node + // is restarted, and at the same time jumps the + // partition, so the side that was now just less than + // a quorum becomes a quorum; 4) the new quorum + // reaches a contradictory consensus. + restartProb: 0, + } + + for i := 0; i < 1000; i++ { + m := makeRandomModel(params, r, t) + + if !m.simulate(params) { + m.t.Fatal("Failed to converge") + } + + m.validate() + } +} From 3cd1a17c239a7422bcfbd525d914077b9c6810c0 Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Wed, 15 Apr 2015 18:00:48 +0100 Subject: [PATCH 3/7] IP Allocation Management Using the Ring CRDT and Weave gossip, provides an interface for the weave script to allocate IP addresses in `weave run`, `weave start` and `weave expose`. See docs/ipam.md for implementation details, and site/features.md for user-visible changes. --- Makefile | 2 +- common/errors.go | 7 + docs/ipam.md | 214 +++++++++++ ipam/address/address.go | 54 +++ ipam/allocate.go | 58 +++ ipam/allocator.go | 694 +++++++++++++++++++++++++++++++++++ ipam/allocator_test.go | 453 +++++++++++++++++++++++ ipam/claim.go | 75 ++++ ipam/dummy.go | 36 ++ ipam/http.go | 72 ++++ ipam/http_test.go | 164 +++++++++ ipam/space/space.go | 248 +++++++++++++ ipam/space/space_test.go | 249 +++++++++++++ ipam/testutils_test.go | 286 +++++++++++++++ ipam/utils/utils.go | 49 --- router/protocol.go | 2 +- site/features.md | 48 +++ test/120_ipam_test.sh | 18 + test/140_ipam_expose_test.sh | 63 ++++ test/250_dns_ipam_test.sh | 27 ++ weave | 123 +++++-- weaver/main.go | 68 +++- 22 files changed, 2930 insertions(+), 80 deletions(-) create mode 100644 docs/ipam.md create mode 100644 ipam/address/address.go create mode 100644 ipam/allocate.go create mode 100644 ipam/allocator.go create mode 100644 ipam/allocator_test.go create mode 100644 ipam/claim.go create mode 100644 ipam/dummy.go create mode 100644 ipam/http.go create mode 100644 ipam/http_test.go create mode 100644 ipam/space/space.go create mode 100644 ipam/space/space_test.go create mode 100644 ipam/testutils_test.go delete mode 100644 ipam/utils/utils.go create mode 100755 test/120_ipam_test.sh create mode 100755 test/140_ipam_expose_test.sh create mode 100755 test/250_dns_ipam_test.sh diff --git a/Makefile b/Makefile index 801afd0c..ed060073 100644 --- a/Makefile +++ b/Makefile @@ -41,7 +41,7 @@ $(WEAVER_EXE) $(WEAVEDNS_EXE): common/*.go false; \ } -$(WEAVER_EXE): router/*.go weaver/main.go +$(WEAVER_EXE): router/*.go ipam/*.go ipam/*/*.go weaver/main.go $(WEAVEDNS_EXE): nameserver/*.go weavedns/main.go # Sigproxy needs separate rule as it fails the netgo check in the main diff --git a/common/errors.go b/common/errors.go index 23072055..a0a9cc96 100644 --- a/common/errors.go +++ b/common/errors.go @@ -11,3 +11,10 @@ func CheckWarn(e error) { Warning.Println(e) } } + +// Assert test is true, panic otherwise +func Assert(test bool) { + if !test { + panic("Assertion failure") + } +} diff --git a/docs/ipam.md b/docs/ipam.md new file mode 100644 index 00000000..706ad842 --- /dev/null +++ b/docs/ipam.md @@ -0,0 +1,214 @@ +See the [requirements](https://github.com/zettio/weave/wiki/IP-allocation-requirements). + +At its highest level, the idea is that we start with a certain IP +address space, known to all peers, and divide it up between the +peers. This allows peers to allocate and free individual IPs locally +until they run out. + +We use a CRDT to represent shared knowledge about the space, +transmitted over the Weave Gossip mechanism, together with +point-to-point messages for one peer to request more space from +another. + +The allocator running at each peer also has an http interface which +the container infrastructure (e.g. the Weave script, or a Docker +plugin) uses to request IP addresses. + +![Schematic of IP allocation in operation](https://docs.google.com/drawings/d/1-EUIRKYxwfKTpBJ7v_LMcdvSpodIMSz4lT3wgEfWKl4/pub?w=701&h=310) + +## Commands + +The commands supported by the allocator via the http interface are: + +- Allocate: request one IP address for a container +- Free: return an IP address that is currently allocated +- Claim: request a specific IP address for a container (e.g. because + it is already using that address) + +The allocator also watches via the Docker event mechanism: if a +container dies then all IP addresses allocated to that container are +freed. + +## Definitions + +1. Allocations. We use the word 'allocation' to refer to a specific + IP address being assigned, e.g. so it can be given to a container. + +2. Range. Most of the time, instead of dealing with individual IP + addresses, we operate on them in contiguous groups, for which we + use the word "range". + +3. Ring. We consider the address space as a ring, so ranges wrap + around from the highest address to the lowest address. + +4. Peer. A Peer is a node on the Weave network. It can own zero or + more ranges. + +### The Allocation Process + +When a peer owns some range(s), it can allocate freely from within +those ranges to containers on the same machine. If it runs out of +space (all owned ranges are full), it will ask another peer for space: + - it picks a peer to ask at random, weighted by the amount of space + owned by each peer + - if the target peer decides to give up space, it unicasts a message + back to the asker with the newly-updated ring. + - if the target peer has no space, it unicasts a message back to the + asker with its current copy of the ring, on the basis that the + requestor must have acted on out-of-date information. + - it will continue to ask for space until it receives some, or its + copy of the ring tells it all peers are full. + +### Claiming an address + +If a Weave process is restarted, in most cases it will hear from +another peer which ranges it used to own, but it needs to know which +individual IP addresses are assigned to containers in order to avoid +giving the same address out in subsequent allocation requests. The +weave script invokes the `claim` command in `populate_ipam` to do +this. + +When the Allocator is told to claim an address, there are four +possibilities: + - the address is outside of the space managed by this Allocator, in + which case we ignore the request. + - the address is owned by this peer, in which case we record the + address as being assigned to a particular container and return + success. + - the address is owned by a different peer, in which case we return + failure. + - we have not yet heard of any address ranges being owned by anyone, + in which case we wait until we do hear. + +This approach fails if the peer does not hear from another peer about +the ranges it used to own, e.g. if all peers in a network partition +are restarted at once. + +### The Ring CRDT + +We use a Convergent Replicated Data Type - a CRDT - so that peers can +make changes concurrently and communicate them without central +coordination. To achieve this, we arrange that peers only make changes +to the data structure in ranges that they own (except under +administrator command - see later). + +The data structure is a set of tokens, each containing the name of an +owning peer. A peer can own many ranges. Each token is placed at the +start address of a range, and the set is kept ordered so each range +goes from one token to the next. Each range on the ring includes the +start address but does not include the end address (which is the start +of the next range). Ranges wrap, so the 'next' token after the last +one is the first token. + +![Tokens on the Ring](https://docs.google.com/drawings/d/1hp--q2vmxbBAnPjhza4Kqjr1ugrw2iS1M1GerhH-IKY/pub?w=960&h=288) + +In more detail: +- Each token is a tuple {peer name, version}, placed + at an IP address. +- Peer names are taken from Weave: they are unique and survive across restarts. +- The contents of a token can only be updated by the owning peer, and + when this is done the version is incremented +- The ring data structure is always gossiped in its entirety +- The merge operation when a peer receives a ring via gossip is: + - Tokens with unique addresses are just copied into the combined ring + - For tokens at the same address, pick the one with the highest + version number +- The data gossiped about the ring also includes the amount of free + space in each range: this is not essential but it improves the + selection of which peer to ask for space. +- When a peer is asked for space, there are four scenarios: + 1. It has an empty range; it can change the peer associated with + the token at the beginning of the range and increment the version. + 2. It has a range which can be subdivided by a single token to form + a free range. It inserts said token, owned by the peer requesting + space. + 3. It has a 'hole' in the middle of a range; an empty range can be + created by inserting two tokens, one at the beginning of the hole + owned by the peer requesting the space, and one at the end of the + hole owned by the requestee. + 4. It has no space. + +## Initialisation + +The previous sections describe how peers coordinate changes to the +ring. But how is the initial state of the ring established? If a new +peer joins a long-standing cluster, it can learn about the ring state +from other peers. But in a freshly started cluster, the initial ring +state must be established from a clean slate. And it must be +consistent across all peers. This is a distributed consensus problem, +and we solve it using the Paxos algorithm. + +Although Paxos has a reputation for being hard to understand and to +implement, the implementation used for ring initialization is +relatively straightforward: + +- We only need to establish consensus for a single value, rather than + a succession of values (as in a replicated transaction log). Thus + we implement basic Paxos, rather than multi-Paxos, and the + implementation closely follows the outline of basic Paxos described + [on + wikipedia](http://en.wikipedia.org/wiki/Paxos_%28computer_science%29#Basic_Paxos). + +- All peers play all the roles described in basic Paxos: Proposer, + acceptor and listener. + +- Paxos is usually described in terms of unicast messages from one + agent to another, although three of the four messages are fanned out + to all agents in a certain role ("prepare" and "accept request" from + a proposer to all acceptors; "accepted" from an acceptor to all + learners). So most implementations involve a communications layer + to implement the required communication patterns. But our + implementation is built on top of the weave gossip layer, which + gives us a broadcast medium. Using broadcast for the one truly + unicast message ("promise") may seem wasteful, but as we only + establish consensus for a single value, it is not a major concern. + +The value the peers obtain consensus on is the set of peers that will +be represented in the initial ring (with each peer getting an equal +share of the address space). When a proposing peer gets to choose the +value, it includes all the peers it has heard from during the Paxos +phase, which is at least a quorum. + +Once a consensus is reached, it is used to initialize the ring. If a +peer hears about an initialized ring via gossip, that implies that a +consensus was reached, so it will stop participating in Paxos. + +Paxos requires that we know the quorum size for the cluster. As weave +clusters are a dynamic entity, there is no fixed quorum size. But the +Paxos phase used to initialize the ring should normally be +short-lived, so we just need to know the initial cluster size. A +heuristic is used to derive this from the list of addresses passed to +`weave launch`, but the user can set it explicitly. + +## Peer shutdown + +When a peer leaves (a `weave reset` command), it grants all its own +tokens to another peer, then broadcasts the updated ring. + +After sending the message, the peer terminates - it does not wait for +any response. + +Failures: +- message lost + - the space will be unusable by other peers because it will still be + seen as owned. + +To cope with the situation where a peer has left or died without +managing to tell its peers, an administrator may go to any other peer +and command that it take over the dead peer's tokens (with `weave +rmpeer`). This information will then be gossipped out to the network. + +(This operation is not guaranteed to be safe - if the dead peer made +some transfers which have are known to other peers but have not +reached the peer that does the `rmpeer`, then we get an inconsistent +ring. We could make it safe, up to partitions, by inquiring of every +live peer what its view of the dead peer is, and making sure we use +the latest info.) + +## Limitations + +Nothing is persisted. If one peer restarts it should receive gossip +from other peers showing what it used to own, and in that case it can +recover quite well. If, however, there are no peers left alive, or +this peer cannot establish network communication with those that are, +then it cannot recover. diff --git a/ipam/address/address.go b/ipam/address/address.go new file mode 100644 index 00000000..b9f47ac3 --- /dev/null +++ b/ipam/address/address.go @@ -0,0 +1,54 @@ +package address + +import ( + "github.com/weaveworks/weave/common" + "net" +) + +// Using 32-bit integer to represent IPv4 address +type Address uint32 +type Offset uint32 + +type Range struct { + Start, End Address // [Start, End); Start <= End +} + +func ParseIP(s string) (Address, error) { + if ip := net.ParseIP(s); ip == nil { + return 0, &net.ParseError{Type: "IP Address", Text: s} + } else { + return FromIP4(ip), nil + } +} + +// FromIP4 converts an ipv4 address to our integer address type +func FromIP4(ip4 net.IP) (r Address) { + for _, b := range ip4.To4() { + r <<= 8 + r |= Address(b) + } + return +} + +// IP4 converts our integer address type to an ipv4 address +func (addr Address) IP4() (r net.IP) { + r = make([]byte, net.IPv4len) + for i := 3; i >= 0; i-- { + r[i] = byte(addr) + addr >>= 8 + } + return +} + +func (addr Address) String() string { + return addr.IP4().String() +} + +func Add(addr Address, i Offset) Address { + return addr + Address(i) +} + +func Subtract(a, b Address) Offset { + common.Assert(a >= b) + return Offset(a - b) +} diff --git a/ipam/allocate.go b/ipam/allocate.go new file mode 100644 index 00000000..320e8e02 --- /dev/null +++ b/ipam/allocate.go @@ -0,0 +1,58 @@ +package ipam + +import ( + "fmt" + "github.com/weaveworks/weave/ipam/address" +) + +type allocateResult struct { + addr address.Address + err error +} + +type allocate struct { + resultChan chan<- allocateResult + hasBeenCancelled func() bool + ident string +} + +// Try returns true if the request is completed, false if pending +func (g *allocate) Try(alloc *Allocator) bool { + if g.hasBeenCancelled() { + g.Cancel() + return true + } + + // If we have previously stored an address for this container, return it. + if addr, found := alloc.owned[g.ident]; found { + g.resultChan <- allocateResult{addr, nil} + return true + } + + if ok, addr := alloc.space.Allocate(); ok { + alloc.debugln("Allocated", addr, "for", g.ident) + alloc.addOwned(g.ident, addr) + g.resultChan <- allocateResult{addr, nil} + return true + } + + // out of space + if donor, err := alloc.ring.ChoosePeerToAskForSpace(); err == nil { + alloc.debugln("Decided to ask peer", donor, "for space") + alloc.sendRequest(donor, msgSpaceRequest) + } + + return false +} + +func (g *allocate) Cancel() { + g.resultChan <- allocateResult{0, fmt.Errorf("Request Cancelled")} +} + +func (g *allocate) String() string { + return fmt.Sprintf("Allocate for %s", g.ident) +} + +func (g *allocate) ForContainer(ident string) bool { + return g.ident == ident +} diff --git a/ipam/allocator.go b/ipam/allocator.go new file mode 100644 index 00000000..28f7f6c7 --- /dev/null +++ b/ipam/allocator.go @@ -0,0 +1,694 @@ +package ipam + +import ( + "bytes" + "encoding/gob" + "errors" + "fmt" + "net" + "sort" + "time" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" + "github.com/weaveworks/weave/ipam/paxos" + "github.com/weaveworks/weave/ipam/ring" + "github.com/weaveworks/weave/ipam/space" + "github.com/weaveworks/weave/router" +) + +// Kinds of message we can unicast to other peers +const ( + msgSpaceRequest = iota + msgRingUpdate + + paxosInterval = time.Second * 5 +) + +// operation represents something which Allocator wants to do, but +// which may need to wait until some other message arrives. +type operation interface { + // Try attempts this operations and returns false if needs to be tried again. + Try(alloc *Allocator) bool + + Cancel() + + String() string + + // Does this operation pertain to the given container id? + // Used for tidying up pending operations when containers die. + ForContainer(ident string) bool +} + +// Allocator brings together Ring and space.Set, and does the +// necessary plumbing. Runs as a single-threaded Actor, so no locks +// are used around data structures. +type Allocator struct { + actionChan chan<- func() + ourName router.PeerName + subnetStart address.Address // start address of space all peers are allocating from + subnetSize address.Offset // length of space all peers are allocating from + prefixLen int // network prefix length, e.g. 24 for a /24 network + ring *ring.Ring // information on ranges owned by all peers + space space.Space // more detail on ranges owned by us + owned map[string]address.Address // who owns what address, indexed by container-ID + nicknames map[router.PeerName]string // so we can map nicknames for rmpeer + pendingAllocates []operation // held until we get some free space + pendingClaims []operation // held until we know who owns the space + gossip router.Gossip // our link to the outside world for sending messages + paxos *paxos.Node + paxosTicker *time.Ticker + shuttingDown bool // to avoid doing any requests while trying to shut down + now func() time.Time +} + +// NewAllocator creates and initialises a new Allocator +func NewAllocator(ourName router.PeerName, ourUID router.PeerUID, ourNickname string, subnetCIDR string, quorum uint) (*Allocator, error) { + _, subnet, err := net.ParseCIDR(subnetCIDR) + if err != nil { + return nil, err + } + if subnet.IP.To4() == nil { + return nil, errors.New("Non-IPv4 address not supported") + } + // Get the size of the network from the mask + ones, bits := subnet.Mask.Size() + var subnetSize address.Offset = 1 << uint(bits-ones) + if subnetSize < 4 { + return nil, errors.New("Allocation subnet too small") + } + subnetStart := address.FromIP4(subnet.IP) + alloc := &Allocator{ + ourName: ourName, + subnetStart: subnetStart, + subnetSize: subnetSize, + prefixLen: ones, + // per RFC 1122, don't allocate the first and last address in the subnet + ring: ring.New(address.Add(subnetStart, 1), address.Add(subnetStart, subnetSize-1), ourName), + owned: make(map[string]address.Address), + paxos: paxos.NewNode(ourName, ourUID, quorum), + nicknames: map[router.PeerName]string{ourName: ourNickname}, + now: time.Now, + } + return alloc, nil +} + +// Start runs the allocator goroutine +func (alloc *Allocator) Start() { + actionChan := make(chan func(), router.ChannelSize) + alloc.actionChan = actionChan + go alloc.actorLoop(actionChan) +} + +// Stop makes the actor routine exit, for test purposes ONLY because any +// calls after this is processed will hang. Async. +func (alloc *Allocator) Stop() { + alloc.actionChan <- nil +} + +// Operation life cycle + +// Given an operation, try it, and add it to the pending queue if it didn't succeed +func (alloc *Allocator) doOperation(op operation, ops *[]operation) { + alloc.actionChan <- func() { + if alloc.shuttingDown { + op.Cancel() + return + } + alloc.establishRing() + if !op.Try(alloc) { + *ops = append(*ops, op) + } + } +} + +// Given an operation, remove it from the pending queue +// Note the op may not be on the queue; it may have +// already succeeded. If it is on the queue, we call +// cancel on it, allowing callers waiting for the resultChans +// to unblock. +func (alloc *Allocator) cancelOp(op operation, ops *[]operation) { + for i, op := range *ops { + if op == op { + *ops = append((*ops)[:i], (*ops)[i+1:]...) + op.Cancel() + break + } + } +} + +// Cancel all operations in a queue +func (alloc *Allocator) cancelOps(ops *[]operation) { + for _, op := range *ops { + op.Cancel() + } + *ops = []operation{} +} + +// Cancel all operations for a given container id, returns true +// if we found any. +func (alloc *Allocator) cancelOpsFor(ops *[]operation, ident string) bool { + var found bool + for i, op := range *ops { + if op.ForContainer(ident) { + found = true + op.Cancel() + *ops = append((*ops)[:i], (*ops)[i+1:]...) + } + } + return found +} + +// Try all pending operations +func (alloc *Allocator) tryPendingOps() { + // The slightly different semantics requires us to operate on 'claims' and + // 'allocates' separately: + // Claims must be tried before Allocates + for i := 0; i < len(alloc.pendingClaims); { + op := alloc.pendingClaims[i] + if !op.Try(alloc) { + i++ + continue + } + alloc.pendingClaims = append(alloc.pendingClaims[:i], alloc.pendingClaims[i+1:]...) + } + + // When the first Allocate fails, bail - no need to + // send too many begs for space. + for i := 0; i < len(alloc.pendingAllocates); { + op := alloc.pendingAllocates[i] + if !op.Try(alloc) { + break + } + alloc.pendingAllocates = append(alloc.pendingAllocates[:i], alloc.pendingAllocates[i+1:]...) + } +} + +func hasBeenCancelled(cancelChan <-chan bool) func() bool { + return func() bool { + select { + case <-cancelChan: + return true + default: + return false + } + } +} + +// Actor client API + +// Allocate (Sync) - get IP address for container with given name +// if there isn't any space we block indefinitely +func (alloc *Allocator) Allocate(ident string, cancelChan <-chan bool) (address.Address, error) { + resultChan := make(chan allocateResult) + op := &allocate{resultChan: resultChan, ident: ident, + hasBeenCancelled: hasBeenCancelled(cancelChan)} + alloc.doOperation(op, &alloc.pendingAllocates) + result := <-resultChan + return result.addr, result.err +} + +// Claim an address that we think we should own (Sync) +func (alloc *Allocator) Claim(ident string, addr address.Address, cancelChan <-chan bool) error { + resultChan := make(chan error) + op := &claim{resultChan: resultChan, ident: ident, addr: addr, + hasBeenCancelled: hasBeenCancelled(cancelChan)} + alloc.doOperation(op, &alloc.pendingClaims) + return <-resultChan +} + +// Free (Sync) - release IP address for container with given name +func (alloc *Allocator) Free(ident string) error { + return alloc.free(ident) +} + +// ContainerDied is provided to satisfy the updater interface; does a free underneath. Async. +func (alloc *Allocator) ContainerDied(ident string) error { + alloc.debugln("Container", ident, "died; releasing addresses") + return alloc.free(ident) +} + +func (alloc *Allocator) free(ident string) error { + errChan := make(chan error) + alloc.actionChan <- func() { + addr, found := alloc.owned[ident] + if found { + alloc.space.Free(addr) + } + delete(alloc.owned, ident) + + // Also remove any pending ops + found = alloc.cancelOpsFor(&alloc.pendingAllocates, ident) || found + found = alloc.cancelOpsFor(&alloc.pendingClaims, ident) || found + + if !found { + errChan <- fmt.Errorf("No addresses for %s", ident) + return + } + errChan <- nil + } + return <-errChan +} + +// Sync. +func (alloc *Allocator) String() string { + resultChan := make(chan string) + alloc.actionChan <- func() { + resultChan <- alloc.string() + } + return <-resultChan +} + +// Shutdown (Sync) +func (alloc *Allocator) Shutdown() { + alloc.infof("Shutdown") + doneChan := make(chan struct{}) + alloc.actionChan <- func() { + alloc.shuttingDown = true + alloc.cancelOps(&alloc.pendingClaims) + alloc.cancelOps(&alloc.pendingAllocates) + if heir := alloc.ring.PickPeerForTransfer(); heir != router.UnknownPeerName { + alloc.ring.Transfer(alloc.ourName, heir) + alloc.space.Clear() + alloc.gossip.GossipBroadcast(alloc.Gossip()) + time.Sleep(100 * time.Millisecond) + } + doneChan <- struct{}{} + } + <-doneChan +} + +// AdminTakeoverRanges (Sync) - take over the ranges owned by a given peer. +// Only done on adminstrator command. +func (alloc *Allocator) AdminTakeoverRanges(peerNameOrNickname string) error { + resultChan := make(chan error) + alloc.actionChan <- func() { + peername, err := alloc.lookupPeername(peerNameOrNickname) + if err != nil { + resultChan <- fmt.Errorf("Cannot find peer '%s'", peerNameOrNickname) + return + } + + alloc.debugln("AdminTakeoverRanges:", peername) + if peername == alloc.ourName { + resultChan <- fmt.Errorf("Cannot take over ranges from yourself!") + return + } + + delete(alloc.nicknames, peername) + err, newRanges := alloc.ring.Transfer(peername, alloc.ourName) + alloc.space.AddRanges(newRanges) + resultChan <- err + } + return <-resultChan +} + +// Lookup a PeerName by nickname or stringified PeerName. We can't +// call into the router for this because we are interested in peers +// that have gone away but are still in the ring, which is why we +// maintain our own nicknames map. +func (alloc *Allocator) lookupPeername(name string) (router.PeerName, error) { + for peername, nickname := range alloc.nicknames { + if nickname == name { + return peername, nil + } + } + + return router.PeerNameFromString(name) +} + +// Restrict the peers in "nicknames" to those in the ring and our own +func (alloc *Allocator) pruneNicknames() { + ringPeers := alloc.ring.PeerNames() + for name, _ := range alloc.nicknames { + if _, ok := ringPeers[name]; !ok && name != alloc.ourName { + delete(alloc.nicknames, name) + } + } +} + +// OnGossipUnicast (Sync) +func (alloc *Allocator) OnGossipUnicast(sender router.PeerName, msg []byte) error { + alloc.debugln("OnGossipUnicast from", sender, ": ", len(msg), "bytes") + resultChan := make(chan error) + alloc.actionChan <- func() { + switch msg[0] { + case msgSpaceRequest: + // some other peer asked us for space + alloc.donateSpace(sender) + resultChan <- nil + case msgRingUpdate: + resultChan <- alloc.update(msg[1:]) + } + } + return <-resultChan +} + +// OnGossipBroadcast (Sync) +func (alloc *Allocator) OnGossipBroadcast(msg []byte) (router.GossipData, error) { + alloc.debugln("OnGossipBroadcast:", len(msg), "bytes") + resultChan := make(chan error) + alloc.actionChan <- func() { + resultChan <- alloc.update(msg) + } + return alloc.Gossip(), <-resultChan +} + +type gossipState struct { + // We send a timstamp along with the information to be + // gossipped in order to do detect skewed clocks + Now int64 + Nicknames map[router.PeerName]string + + Paxos paxos.GossipState + Ring *ring.Ring +} + +func (alloc *Allocator) encode() []byte { + data := gossipState{ + Now: alloc.now().Unix(), + Nicknames: alloc.nicknames, + } + + // We're only interested in Paxos until we have a Ring. + if alloc.ring.Empty() { + data.Paxos = alloc.paxos.GossipState() + } else { + data.Ring = alloc.ring + } + buf := new(bytes.Buffer) + enc := gob.NewEncoder(buf) + if err := enc.Encode(data); err != nil { + panic(err) + } + return buf.Bytes() +} + +// Encode (Sync) +func (alloc *Allocator) Encode() []byte { + resultChan := make(chan []byte) + alloc.actionChan <- func() { + resultChan <- alloc.encode() + } + return <-resultChan +} + +// OnGossip (Sync) +func (alloc *Allocator) OnGossip(msg []byte) (router.GossipData, error) { + alloc.debugln("Allocator.OnGossip:", len(msg), "bytes") + resultChan := make(chan error) + alloc.actionChan <- func() { + resultChan <- alloc.update(msg) + } + return nil, <-resultChan // for now, we never propagate updates. TBD +} + +// GossipData implementation is trivial - we always gossip the latest +// data we have at time of sending +type ipamGossipData struct { + alloc *Allocator +} + +func (d *ipamGossipData) Merge(other router.GossipData) { + // no-op +} + +func (d *ipamGossipData) Encode() []byte { + return d.alloc.Encode() +} + +// Gossip returns a GossipData implementation, which in this case always +// returns the latest ring state (and does nothing on merge) +func (alloc *Allocator) Gossip() router.GossipData { + return &ipamGossipData{alloc} +} + +// SetInterfaces gives the allocator two interfaces for talking to the outside world +func (alloc *Allocator) SetInterfaces(gossip router.Gossip) { + alloc.gossip = gossip +} + +// ACTOR server + +func (alloc *Allocator) actorLoop(actionChan <-chan func()) { + for { + var tickChan <-chan time.Time + if alloc.paxosTicker != nil { + tickChan = alloc.paxosTicker.C + } + + select { + case action := <-actionChan: + if action == nil { + return + } + action() + case <-tickChan: + alloc.propose() + } + + alloc.assertInvariants() + alloc.reportFreeSpace() + } +} + +// Helper functions + +func (alloc *Allocator) string() string { + var buf bytes.Buffer + fmt.Fprintf(&buf, "Allocator subnet %s+%d\n", alloc.subnetStart.String(), alloc.subnetSize) + + if alloc.ring.Empty() { + fmt.Fprintf(&buf, "Paxos: %s", alloc.paxos.String()) + } else { + localFreeSpace := alloc.space.NumFreeAddresses() + remoteFreeSpace := alloc.ring.TotalRemoteFree() + percentFree := 100 * float64(localFreeSpace+remoteFreeSpace) / float64(alloc.subnetSize) + fmt.Fprintf(&buf, " Free IPs: ~%.1f%%, %d local, ~%d remote\n", + percentFree, localFreeSpace, remoteFreeSpace) + + alloc.ring.FprintWithNicknames(&buf, alloc.nicknames) + fmt.Fprintf(&buf, alloc.space.String()) + if len(alloc.pendingAllocates)+len(alloc.pendingClaims) > 0 { + fmt.Fprintf(&buf, "\nPending requests for ") + } + for _, op := range alloc.pendingAllocates { + fmt.Fprintf(&buf, "%s, ", op.String()) + } + for _, op := range alloc.pendingClaims { + fmt.Fprintf(&buf, "%s, ", op.String()) + } + } + return buf.String() +} + +func (alloc *Allocator) consensus() []router.PeerName { + _, val := alloc.paxos.Consensus() + return val.Value +} + +// Ensure we are making progress towards an established ring +func (alloc *Allocator) establishRing() { + if !alloc.ring.Empty() || alloc.paxosTicker != nil { + return + } + + alloc.propose() + if cons := alloc.consensus(); cons != nil { + // If the quorum was 1, then proposing immediately + // leads to consensus + alloc.createRing(cons) + } else { + // re-try until we get consensus + alloc.paxosTicker = time.NewTicker(paxosInterval) + } +} + +func (alloc *Allocator) createRing(peers []router.PeerName) { + alloc.debugln("Paxos consensus:", peers) + alloc.ring.ClaimForPeers(normalizeConsensus(peers)) + alloc.gossip.GossipBroadcast(alloc.Gossip()) + alloc.ringUpdated() +} + +func (alloc *Allocator) ringUpdated() { + // When we have a ring, we don't need paxos any more + if alloc.paxos != nil { + alloc.paxos = nil + + if alloc.paxosTicker != nil { + alloc.paxosTicker.Stop() + alloc.paxosTicker = nil + } + } + + alloc.space.UpdateRanges(alloc.ring.OwnedRanges()) + alloc.tryPendingOps() +} + +// For compatibility with sort.Interface +type peerNames []router.PeerName + +func (a peerNames) Len() int { return len(a) } +func (a peerNames) Less(i, j int) bool { return a[i] < a[j] } +func (a peerNames) Swap(i, j int) { a[i], a[j] = a[j], a[i] } + +// When we get a consensus from Paxos, the peer names are not in a +// defined order and may contain duplicates. This function sorts them +// and de-dupes. +func normalizeConsensus(consensus []router.PeerName) []router.PeerName { + if len(consensus) == 0 { + return nil + } + + peers := make(peerNames, len(consensus)) + copy(peers, consensus) + sort.Sort(peers) + + dst := 0 + for src := 1; src < len(peers); src++ { + if peers[dst] != peers[src] { + dst++ + peers[dst] = peers[src] + } + } + + return peers[:dst+1] +} + +func (alloc *Allocator) propose() { + alloc.debugf("Paxos proposing") + alloc.paxos.Propose() + alloc.gossip.GossipBroadcast(alloc.Gossip()) +} + +func (alloc *Allocator) sendRequest(dest router.PeerName, kind byte) { + msg := router.Concat([]byte{kind}, alloc.encode()) + alloc.gossip.GossipUnicast(dest, msg) +} + +func (alloc *Allocator) update(msg []byte) error { + reader := bytes.NewReader(msg) + decoder := gob.NewDecoder(reader) + var data gossipState + var err error + + if err := decoder.Decode(&data); err != nil { + return err + } + + deltat := time.Unix(data.Now, 0).Sub(alloc.now()) + if deltat > time.Hour || -deltat > time.Hour { + return fmt.Errorf("clock skew of %v detected, ignoring update", deltat) + } + + // Merge nicknames + for peer, nickname := range data.Nicknames { + alloc.nicknames[peer] = nickname + } + + // only one of Ring and Paxos should be present. And we + // shouldn't get updates for a empty Ring. But tolerate + // them just in case. + if data.Ring != nil { + err = alloc.ring.Merge(*data.Ring) + if !alloc.ring.Empty() { + alloc.pruneNicknames() + alloc.ringUpdated() + } + return err + } + + if data.Paxos != nil && alloc.ring.Empty() { + if alloc.paxos.Update(data.Paxos) { + if alloc.paxos.Think() { + // If something important changed, broadcast + alloc.gossip.GossipBroadcast(alloc.Gossip()) + } + + if cons := alloc.consensus(); cons != nil { + alloc.createRing(cons) + } + } + } + + return nil +} + +func (alloc *Allocator) donateSpace(to router.PeerName) { + // No matter what we do, we'll send a unicast gossip + // of our ring back to tha chap who asked for space. + // This serves to both tell him of any space we might + // have given him, or tell him where he might find some + // more. + defer alloc.sendRequest(to, msgRingUpdate) + + alloc.debugln("Peer", to, "asked me for space") + start, size, ok := alloc.space.Donate() + if !ok { + free := alloc.space.NumFreeAddresses() + common.Assert(free == 0) + alloc.debugln("No space to give to peer", to) + return + } + end := address.Add(start, size) + alloc.debugln("Giving range", start, end, size, "to", to) + alloc.ring.GrantRangeToHost(start, end, to) +} + +func (alloc *Allocator) assertInvariants() { + // We need to ensure all ranges the ring thinks we own have + // a corresponding space in the space set, and vice versa + checkSpace := space.New() + checkSpace.AddRanges(alloc.ring.OwnedRanges()) + ranges := checkSpace.OwnedRanges() + spaces := alloc.space.OwnedRanges() + + common.Assert(len(ranges) == len(spaces)) + + for i := 0; i < len(ranges); i++ { + r := ranges[i] + s := spaces[i] + common.Assert(s.Start == r.Start && s.End == r.End) + } +} + +func (alloc *Allocator) reportFreeSpace() { + ranges := alloc.ring.OwnedRanges() + if len(ranges) == 0 { + return + } + + freespace := make(map[address.Address]address.Offset) + for _, r := range ranges { + freespace[r.Start] = alloc.space.NumFreeAddressesInRange(r.Start, r.End) + } + alloc.ring.ReportFree(freespace) +} + +// Owned addresses + +func (alloc *Allocator) addOwned(ident string, addr address.Address) { + alloc.owned[ident] = addr +} + +func (alloc *Allocator) findOwner(addr address.Address) string { + for ident, candidate := range alloc.owned { + if candidate == addr { + return ident + } + } + return "" +} + +// Logging + +func (alloc *Allocator) infof(fmt string, args ...interface{}) { + common.Info.Printf("[allocator %s] "+fmt, append([]interface{}{alloc.ourName}, args...)...) +} +func (alloc *Allocator) debugln(args ...interface{}) { + common.Debug.Println(append([]interface{}{fmt.Sprintf("[allocator %s]:", alloc.ourName)}, args...)...) +} +func (alloc *Allocator) debugf(fmt string, args ...interface{}) { + common.Debug.Printf("[allocator %s] "+fmt, append([]interface{}{alloc.ourName}, args...)...) +} diff --git a/ipam/allocator_test.go b/ipam/allocator_test.go new file mode 100644 index 00000000..b3c550c8 --- /dev/null +++ b/ipam/allocator_test.go @@ -0,0 +1,453 @@ +package ipam + +import ( + "fmt" + "math/rand" + "sync" + "testing" + "time" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" + "github.com/weaveworks/weave/router" + wt "github.com/weaveworks/weave/testing" +) + +const ( + testStart1 = "10.0.1.0" + testStart2 = "10.0.2.0" + testStart3 = "10.0.3.0" +) + +func TestAllocFree(t *testing.T) { + const ( + container1 = "abcdef" + container2 = "baddf00d" + container3 = "b01df00d" + universe = "10.0.3.0/28" + testAddr1 = "10.0.3.1" // first address allocated should be .1 because .0 is network addr + spaceSize = 14 // 16 IP addresses in /28, minus .0 and .15 + ) + + alloc := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", universe, 1) + defer alloc.Stop() + + alloc.claimRingForTesting() + addr1, _ := alloc.Allocate(container1, nil) + wt.AssertEqualString(t, addr1.String(), testAddr1, "address") + + // Ask for another address for a different container and check it's different + addr2, _ := alloc.Allocate(container2, nil) + if addr2.String() == testAddr1 { + t.Fatalf("Expected different address but got %s", addr2.String()) + } + + // Ask for the first container again and we should get the same address again + addr1a, _ := alloc.Allocate(container1, nil) + wt.AssertEqualString(t, addr1a.String(), testAddr1, "address") + + // Now free the first one, and we should get it back when we ask + wt.AssertSuccess(t, alloc.Free(container1)) + addr3, _ := alloc.Allocate(container3, nil) + wt.AssertEqualString(t, addr3.String(), testAddr1, "address") + + alloc.ContainerDied(container2) + alloc.ContainerDied(container3) + alloc.String() // force sync-up after async call + wt.AssertEquals(t, alloc.space.NumFreeAddresses(), address.Offset(spaceSize)) +} + +func TestBootstrap(t *testing.T) { + common.InitDefaultLogging(false) + const ( + donateSize = 5 + donateStart = "10.0.1.7" + ourNameString = "01:00:00:01:00:00" + peerNameString = "02:00:00:02:00:00" + ) + + alloc1 := makeAllocatorWithMockGossip(t, ourNameString, testStart1+"/22", 2) + defer alloc1.Stop() + + // Simulate another peer on the gossip network + alloc2 := makeAllocatorWithMockGossip(t, peerNameString, testStart1+"/22", 2) + defer alloc2.Stop() + + alloc1.OnGossipBroadcast(alloc2.encode()) + + alloc1.tryPendingOps() + + ExpectBroadcastMessage(alloc1, nil) // alloc1 will try to form consensus + done := make(chan bool) + go func() { + alloc1.Allocate("somecontainer", nil) + done <- true + }() + time.Sleep(100 * time.Millisecond) + AssertNothingSent(t, done) + + CheckAllExpectedMessagesSent(alloc1, alloc2) + + alloc1.tryPendingOps() + AssertNothingSent(t, done) + + CheckAllExpectedMessagesSent(alloc1, alloc2) + + // alloc2 receives paxos update and broadcasts its reply + ExpectBroadcastMessage(alloc2, nil) + alloc2.OnGossipBroadcast(alloc1.encode()) + + ExpectBroadcastMessage(alloc1, nil) + alloc1.OnGossipBroadcast(alloc2.encode()) + + // both nodes will get consensus now so initialize the ring + ExpectBroadcastMessage(alloc2, nil) + ExpectBroadcastMessage(alloc2, nil) + alloc2.OnGossipBroadcast(alloc1.encode()) + + CheckAllExpectedMessagesSent(alloc1, alloc2) + + alloc1.OnGossipBroadcast(alloc2.encode()) + // now alloc1 should have space + + AssertSent(t, done) + + CheckAllExpectedMessagesSent(alloc1, alloc2) +} + +func TestAllocatorClaim(t *testing.T) { + const ( + container1 = "abcdef" + container2 = "baddf00d" + container3 = "b01df00d" + universe = "10.0.3.0/30" + testAddr1 = "10.0.3.1" // first address allocated should be .1 because .0 is network addr + ) + + alloc := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", universe, 1) + defer alloc.Stop() + + alloc.claimRingForTesting() + addr1, _ := alloc.Allocate(container1, nil) + alloc.Allocate(container2, nil) + + // Now free the first one, and try to claim it + wt.AssertSuccess(t, alloc.Free(container1)) + t.Log(alloc) + err := alloc.Claim(container3, addr1, nil) + wt.AssertNoErr(t, err) + addr3, _ := alloc.Allocate(container3, nil) + wt.AssertEqualString(t, addr3.String(), testAddr1, "address") +} + +func (alloc *Allocator) pause() func() { + paused := make(chan struct{}) + alloc.actionChan <- func() { + paused <- struct{}{} + <-paused + } + <-paused + return func() { + close(paused) + } +} + +func TestCancel(t *testing.T) { + common.InitDefaultLogging(false) + const ( + CIDR = "10.0.1.7/26" + ) + + router := TestGossipRouter{make(map[router.PeerName]chan gossipMessage), 0.0} + + alloc1 := makeAllocator("01:00:00:02:00:00", CIDR, 2) + alloc1.SetInterfaces(router.connect(alloc1.ourName, alloc1)) + + alloc2 := makeAllocator("02:00:00:02:00:00", CIDR, 2) + alloc2.SetInterfaces(router.connect(alloc2.ourName, alloc2)) + alloc1.claimRingForTesting(alloc1, alloc2) + alloc2.claimRingForTesting(alloc1, alloc2) + + alloc1.Start() + alloc2.Start() + + // tell peers about each other + alloc1.OnGossipBroadcast(alloc2.encode()) + + // Get some IPs, so each allocator has some space + res1, _ := alloc1.Allocate("foo", nil) + common.Debug.Printf("res1 = %s", res1.String()) + res2, _ := alloc2.Allocate("bar", nil) + common.Debug.Printf("res2 = %s", res2.String()) + if res1 == res2 { + wt.Fatalf(t, "Error: got same ips!") + } + + // Now we're going to pause alloc2 and ask alloc1 + // for an allocation + unpause := alloc2.pause() + + // Use up all the IPs that alloc1 owns, so the allocation after this will prompt a request to alloc2 + for i := 0; alloc1.space.NumFreeAddresses() > 0; i++ { + alloc1.Allocate(fmt.Sprintf("tmp%d", i), nil) + } + cancelChan := make(chan bool, 1) + doneChan := make(chan bool) + go func() { + _, ok := alloc1.Allocate("baz", cancelChan) + doneChan <- ok == nil + }() + + AssertNothingSent(t, doneChan) + time.Sleep(100 * time.Millisecond) + AssertNothingSent(t, doneChan) + + cancelChan <- true + unpause() + if <-doneChan { + wt.Fatalf(t, "Error: got result from Allocate") + } +} + +func TestGossipShutdown(t *testing.T) { + const ( + container1 = "abcdef" + container2 = "baddf00d" + universe = "10.0.3.0/30" + testAddr1 = "10.0.3.1" // first address allocated should be .1 because .0 is network addr + ) + + alloc := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", universe, 1) + defer alloc.Stop() + + alloc.claimRingForTesting() + addr1, _ := alloc.Allocate(container1, nil) + wt.AssertEqualString(t, addr1.String(), testAddr1, "address") + + alloc.Shutdown() + + _, err := alloc.Allocate(container2, nil) // trying to allocate after shutdown should fail + wt.AssertFalse(t, err == nil, "no address") + + CheckAllExpectedMessagesSent(alloc) +} + +func TestTransfer(t *testing.T) { + const ( + cidr = "10.0.1.7/22" + ) + allocs, router := makeNetworkOfAllocators(3, cidr) + alloc1 := allocs[0] + alloc2 := allocs[1] + alloc3 := allocs[2] // This will be 'master' and get the first range + + _, err := alloc2.Allocate("foo", nil) + wt.AssertTrue(t, err == nil, "Failed to get address") + + _, err = alloc3.Allocate("bar", nil) + wt.AssertTrue(t, err == nil, "Failed to get address") + + router.GossipBroadcast(alloc2.Gossip()) + router.GossipBroadcast(alloc3.Gossip()) + router.removePeer(alloc2.ourName) + router.removePeer(alloc3.ourName) + alloc2.Stop() + alloc3.Stop() + wt.AssertSuccess(t, alloc1.AdminTakeoverRanges(alloc2.ourName.String())) + wt.AssertSuccess(t, alloc1.AdminTakeoverRanges(alloc3.ourName.String())) + + wt.AssertEquals(t, alloc1.space.NumFreeAddresses(), address.Offset(1022)) + + _, err = alloc1.Allocate("foo", nil) + wt.AssertTrue(t, err == nil, "Failed to get address") + alloc1.Stop() +} + +func TestFakeRouterSimple(t *testing.T) { + common.InitDefaultLogging(false) + const ( + cidr = "10.0.1.7/22" + ) + allocs, _ := makeNetworkOfAllocators(2, cidr) + defer stopNetworkOfAllocators(allocs) + + alloc1 := allocs[0] + //alloc2 := allocs[1] + + addr, _ := alloc1.Allocate("foo", nil) + println("Got addr", addr) +} + +func TestAllocatorFuzz(t *testing.T) { + common.InitDefaultLogging(false) + const ( + firstpass = 1000 + secondpass = 5000 + nodes = 10 + maxAddresses = 1000 + concurrency = 10 + cidr = "10.0.1.7/22" + ) + allocs, _ := makeNetworkOfAllocators(nodes, cidr) + defer stopNetworkOfAllocators(allocs) + + // Test state + // For each IP issued we store the allocator + // that issued it and the name of the container + // it was issued to. + type result struct { + name string + alloc int32 + } + stateLock := sync.Mutex{} + state := make(map[string]result) + // Keep a list of addresses issued, so we + // Can pick random ones + var addrs []string + numPending := 0 + + rand.Seed(0) + + // Remove item from list by swapping it with last + // and reducing slice length by 1 + rm := func(xs []string, i int32) []string { + ls := len(xs) - 1 + xs[i] = xs[ls] + return xs[:ls] + } + + // Do a Allocate and check the address + // is unique. Needs a unique container + // name. + allocate := func(name string) { + stateLock.Lock() + if len(addrs)+numPending >= maxAddresses { + stateLock.Unlock() + return + } + numPending++ + stateLock.Unlock() + + allocIndex := rand.Int31n(nodes) + alloc := allocs[allocIndex] + //common.Info.Printf("Allocate: asking allocator %d", allocIndex) + addr, err := alloc.Allocate(name, nil) + + if err != nil { + panic(fmt.Sprintf("Could not allocate addr")) + } + + //common.Info.Printf("Allocate: got address %s for name %s", addr, name) + addrStr := addr.String() + + stateLock.Lock() + defer stateLock.Unlock() + + if res, existing := state[addrStr]; existing { + panic(fmt.Sprintf("Dup found for address %s - %s and %s", addrStr, + name, res.name)) + } + + state[addrStr] = result{name, allocIndex} + addrs = append(addrs, addrStr) + numPending-- + } + + // Free a random address. + free := func() { + stateLock.Lock() + if len(addrs) == 0 { + stateLock.Unlock() + return + } + // Delete an existing allocation + // Pick random addr + addrIndex := rand.Int31n(int32(len(addrs))) + addr := addrs[addrIndex] + res := state[addr] + addrs = rm(addrs, addrIndex) + delete(state, addr) + stateLock.Unlock() + + alloc := allocs[res.alloc] + //common.Info.Printf("Freeing %s on allocator %d", addr, res.alloc) + + wt.AssertSuccess(t, alloc.Free(res.name)) + } + + // Do a Allocate on an existing container & allocator + // and check we get the right answer. + allocateAgain := func() { + stateLock.Lock() + addrIndex := rand.Int31n(int32(len(addrs))) + addr := addrs[addrIndex] + res := state[addr] + stateLock.Unlock() + alloc := allocs[res.alloc] + + //common.Info.Printf("Asking for %s on allocator %d again", addr, res.alloc) + + newAddr, _ := alloc.Allocate(res.name, nil) + oldAddr, _ := address.ParseIP(addr) + if newAddr != oldAddr { + panic(fmt.Sprintf("Got different address for repeat request")) + } + } + + // Run function _f_ _iterations_ times, in _concurrency_ + // number of goroutines + doConcurrentIterations := func(iterations int, f func(int)) { + iterationsPerThread := iterations / concurrency + + wg := sync.WaitGroup{} + for i := 0; i < concurrency; i++ { + wg.Add(1) + go func(j int) { + defer wg.Done() + for k := 0; k < iterationsPerThread; k++ { + f((j * iterationsPerThread) + k) + } + }(i) + } + wg.Wait() + } + + // First pass, just allocate a bunch of ips + doConcurrentIterations(firstpass, func(iteration int) { + name := fmt.Sprintf("first%d", iteration) + allocate(name) + }) + + // Second pass, random ask for more allocations, + // or remove existing ones, or ask for allocation + // again. + doConcurrentIterations(secondpass, func(iteration int) { + r := rand.Float32() + switch { + case 0.0 <= r && r < 0.4: + // Ask for a new allocation + name := fmt.Sprintf("second%d", iteration) + allocate(name) + + case (0.4 <= r && r < 0.8): + // free a random addr + free() + + case 0.8 <= r && r < 1.0: + // ask for an existing name again, check we get same ip + allocateAgain() + } + }) +} + +func TestGossipSkew(t *testing.T) { + alloc1 := makeAllocatorWithMockGossip(t, "01:00:00:01:00:00", "10.0.1.0/22", 2) + defer alloc1.Stop() + alloc2 := makeAllocatorWithMockGossip(t, "02:00:00:02:00:00", "10.0.1.0/22", 2) + alloc2.now = func() time.Time { return time.Now().Add(time.Hour * 2) } + defer alloc2.Stop() + + if _, err := alloc1.OnGossipBroadcast(alloc2.encode()); err == nil { + t.Fail() + } +} diff --git a/ipam/claim.go b/ipam/claim.go new file mode 100644 index 00000000..ec7bc2bf --- /dev/null +++ b/ipam/claim.go @@ -0,0 +1,75 @@ +package ipam + +import ( + "fmt" + + "github.com/weaveworks/weave/ipam/address" + "github.com/weaveworks/weave/router" +) + +type claim struct { + resultChan chan<- error + hasBeenCancelled func() bool + ident string + addr address.Address +} + +// Try returns true for success (or failure), false if we need to try again later +func (c *claim) Try(alloc *Allocator) bool { + if (c.hasBeenCancelled)() { + c.Cancel() + return true + } + + if !alloc.ring.Contains(c.addr) { + // Address not within our universe; assume user knows what they are doing + alloc.infof("Ignored address %s claimed by %s - not in our universe\n", c.addr, c.ident) + c.resultChan <- nil + return true + } + + // If our ring doesn't know, it must be empty. We will have initiated the + // bootstrap of the ring, so wait until we find some owner for this + // range (might be us). + owner := alloc.ring.Owner(c.addr) + if owner == router.UnknownPeerName { + alloc.infof("Ring is empty; will try later.\n", c.addr, owner) + return false + } + if owner != alloc.ourName { + c.resultChan <- fmt.Errorf("Address %s is owned by other peer %s", c.addr.String(), owner) + return true + } + // We are the owner, check we haven't given it to another container + existingIdent := alloc.findOwner(c.addr) + if existingIdent == c.ident { + // same identifier is claiming same address; that's OK + c.resultChan <- nil + return true + } + if existingIdent == "" { + err := alloc.space.Claim(c.addr) + if err != nil { + c.resultChan <- err + return true + } + alloc.addOwned(c.ident, c.addr) + c.resultChan <- nil + return true + } + // Addr already owned by container on this machine + c.resultChan <- fmt.Errorf("Claimed address %s is already owned by %s", c.addr.String(), existingIdent) + return true +} + +func (c *claim) Cancel() { + c.resultChan <- fmt.Errorf("Operation cancelled.") +} + +func (c *claim) String() string { + return fmt.Sprintf("Claim %s -> %s", c.ident, c.addr.String()) +} + +func (c *claim) ForContainer(ident string) bool { + return c.ident == ident +} diff --git a/ipam/dummy.go b/ipam/dummy.go new file mode 100644 index 00000000..d9761c4d --- /dev/null +++ b/ipam/dummy.go @@ -0,0 +1,36 @@ +package ipam + +import ( + "errors" + + "github.com/weaveworks/weave/router" +) + +// Exists solely for the purpose of detecting a network in which some +// peers are configured with IPAM on and some with IPAM off. +type DummyAllocator struct { +} + +var ( + ErrMixedConfig = errors.New("Mixed IPAM/non-IPAM configuration not supported") +) + +func (alloc *DummyAllocator) OnGossipUnicast(sender router.PeerName, msg []byte) error { + return ErrMixedConfig +} + +func (alloc *DummyAllocator) OnGossipBroadcast(msg []byte) (router.GossipData, error) { + return nil, ErrMixedConfig +} + +func (alloc *DummyAllocator) OnGossip(msg []byte) (router.GossipData, error) { + return nil, ErrMixedConfig +} + +func (alloc *DummyAllocator) Gossip() router.GossipData { + return nil +} + +func (alloc *DummyAllocator) Encode() []byte { + return nil +} diff --git a/ipam/http.go b/ipam/http.go new file mode 100644 index 00000000..39efa892 --- /dev/null +++ b/ipam/http.go @@ -0,0 +1,72 @@ +package ipam + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" +) + +func badRequest(w http.ResponseWriter, err error) { + http.Error(w, err.Error(), http.StatusBadRequest) + common.Warning.Println(err.Error()) +} + +// HandleHTTP wires up ipams HTTP endpoints to the provided mux. +func (alloc *Allocator) HandleHTTP(router *mux.Router) { + router.Methods("PUT").Path("/ip/{id}/{ip}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + closedChan := w.(http.CloseNotifier).CloseNotify() + vars := mux.Vars(r) + ident := vars["id"] + ipStr := vars["ip"] + if ip, err := address.ParseIP(ipStr); err != nil { + badRequest(w, err) + return + } else if err := alloc.Claim(ident, ip, closedChan); err != nil { + badRequest(w, fmt.Errorf("Unable to claim IP address %s: %s", ip, err)) + return + } + + w.WriteHeader(204) + }) + + router.Methods("POST").Path("/ip/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + closedChan := w.(http.CloseNotifier).CloseNotify() + ident := mux.Vars(r)["id"] + newAddr, err := alloc.Allocate(ident, closedChan) + if err != nil { + badRequest(w, err) + return + } + + fmt.Fprintf(w, "%s/%d", newAddr.String(), alloc.prefixLen) + }) + + router.Methods("DELETE").Path("/ip/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ident := mux.Vars(r)["id"] + if err := alloc.Free(ident); err != nil { + badRequest(w, err) + return + } + + w.WriteHeader(204) + }) + + router.Methods("DELETE").Path("/peer").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + alloc.Shutdown() + w.WriteHeader(204) + }) + + router.Methods("DELETE").Path("/peer/{id}").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + ident := mux.Vars(r)["id"] + if err := alloc.AdminTakeoverRanges(ident); err != nil { + badRequest(w, err) + return + } + + w.WriteHeader(204) + }) +} diff --git a/ipam/http_test.go b/ipam/http_test.go new file mode 100644 index 00000000..710bb4fb --- /dev/null +++ b/ipam/http_test.go @@ -0,0 +1,164 @@ +package ipam + +import ( + "fmt" + "io" + "io/ioutil" + "math/rand" + "net/http" + "strings" + "testing" + "time" + + "github.com/gorilla/mux" + + "github.com/weaveworks/weave/common" + wt "github.com/weaveworks/weave/testing" +) + +func HTTPPost(t *testing.T, url string) string { + resp, err := http.Post(url, "", nil) + wt.AssertNoErr(t, err) + wt.AssertStatus(t, resp.StatusCode, http.StatusOK, "http response") + defer resp.Body.Close() + body, _ := ioutil.ReadAll(resp.Body) + return string(body) +} + +func doHTTP(method string, url string) (resp *http.Response, err error) { + req, _ := http.NewRequest(method, url, nil) + return http.DefaultClient.Do(req) +} + +func listenHTTP(port int, alloc *Allocator) { + router := mux.NewRouter() + router.HandleFunc("/status", func(w http.ResponseWriter, r *http.Request) { + io.WriteString(w, fmt.Sprintln(alloc)) + }) + alloc.HandleHTTP(router) + + srv := &http.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: router, + } + if err := srv.ListenAndServe(); err != nil { + common.Error.Fatal("Unable to create http listener: ", err) + } +} + +func allocURL(port int, containerID string) string { + return fmt.Sprintf("http://localhost:%d/ip/%s", port, containerID) +} + +func TestHttp(t *testing.T) { + var ( + containerID = "deadbeef" + container2 = "baddf00d" + container3 = "b01df00d" + testAddr1 = "10.0.3.9" + netSuffix = "/29" + testCIDR1 = "10.0.3.8" + netSuffix + ) + + alloc := makeAllocatorWithMockGossip(t, "08:00:27:01:c3:9a", testCIDR1, 1) + port := rand.Intn(10000) + 32768 + fmt.Println("Http test on port", port) + go listenHTTP(port, alloc) + + time.Sleep(100 * time.Millisecond) // Allow for http server to get going + + alloc.claimRingForTesting() + // Ask the http server for a new address + cidr1 := HTTPPost(t, allocURL(port, containerID)) + wt.AssertEqualString(t, cidr1, testAddr1+netSuffix, "address") + + // Ask the http server for another address and check it's different + cidr2 := HTTPPost(t, allocURL(port, container2)) + wt.AssertNotEqualString(t, cidr2, testAddr1+netSuffix, "address") + + // Ask for the first container again and we should get the same address again + cidr1a := HTTPPost(t, allocURL(port, containerID)) + wt.AssertEqualString(t, cidr1a, testAddr1+netSuffix, "address") + + // Now free the first one, and we should get it back when we ask + doHTTP("DELETE", allocURL(port, containerID)) + cidr3 := HTTPPost(t, allocURL(port, container3)) + wt.AssertEqualString(t, cidr3, testAddr1+netSuffix, "address") + + // Would like to shut down the http server at the end of this test + // but it's complicated. + // See https://groups.google.com/forum/#!topic/golang-nuts/vLHWa5sHnCE +} + +func TestBadHttp(t *testing.T) { + var ( + containerID = "deadbeef" + testCIDR1 = "10.0.0.0/8" + ) + + alloc := makeAllocatorWithMockGossip(t, "08:00:27:01:c3:9a", testCIDR1, 1) + defer alloc.Stop() + port := rand.Intn(10000) + 32768 + fmt.Println("BadHttp test on port", port) + go listenHTTP(port, alloc) + + alloc.claimRingForTesting() + cidr1 := HTTPPost(t, allocURL(port, containerID)) + parts := strings.Split(cidr1, "/") + testAddr1 := parts[0] + // Verb that's not handled + resp, err := doHTTP("HEAD", fmt.Sprintf("http://localhost:%d/ip/%s/%s", port, containerID, testAddr1)) + wt.AssertNoErr(t, err) + wt.AssertStatus(t, resp.StatusCode, http.StatusNotFound, "http response") + // Mis-spelled URL + resp, err = doHTTP("POST", fmt.Sprintf("http://localhost:%d/xip/%s/", port, containerID)) + wt.AssertNoErr(t, err) + wt.AssertStatus(t, resp.StatusCode, http.StatusNotFound, "http response") + // Malformed URL + resp, err = doHTTP("POST", fmt.Sprintf("http://localhost:%d/ip/%s/foo/bar/baz", port, containerID)) + wt.AssertNoErr(t, err) + wt.AssertStatus(t, resp.StatusCode, http.StatusNotFound, "http response") +} + +func TestHTTPCancel(t *testing.T) { + wt.RunWithTimeout(t, 2*time.Second, func() { + impTestHTTPCancel(t) + }) +} + +func impTestHTTPCancel(t *testing.T) { + common.InitDefaultLogging(true) + var ( + containerID = "deadbeef" + testCIDR1 = "10.0.3.5/29" + ) + + alloc := makeAllocatorWithMockGossip(t, "08:00:27:01:c3:9a", testCIDR1, 1) + defer alloc.Stop() + alloc.claimRingForTesting() + port := rand.Intn(10000) + 32768 + fmt.Println("Http test on port", port) + go listenHTTP(port, alloc) + + time.Sleep(100 * time.Millisecond) // Allow for http server to get going + + // Stop the alloc so nothing actually works + unpause := alloc.pause() + + // Ask the http server for a new address + done := make(chan *http.Response) + req, _ := http.NewRequest("POST", allocURL(port, containerID), nil) + go func() { + res, _ := http.DefaultClient.Do(req) + done <- res + }() + + time.Sleep(100 * time.Millisecond) + fmt.Println("Cancelling allocate") + http.DefaultTransport.(*http.Transport).CancelRequest(req) + unpause() + res := <-done + if res != nil { + wt.Fatalf(t, "Error: Allocate returned non-nil") + } +} diff --git a/ipam/space/space.go b/ipam/space/space.go new file mode 100644 index 00000000..2848a033 --- /dev/null +++ b/ipam/space/space.go @@ -0,0 +1,248 @@ +package space + +import ( + "bytes" + "fmt" + "sort" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/ipam/address" +) + +type Addr address.Address + +type Space struct { + // ours and free represent a set of addresses as a sorted + // sequences of ranges. Even elements give the inclusive + // starting points of ranges, and odd elements give the + // exclusive ending points. Ranges in an array do not + // overlap, and neighbouring ranges are always coalesced if + // possible, so the arrays consist of sorted Addrs without + // repetition. + ours []address.Address + free []address.Address +} + +func New() *Space { + return &Space{} +} + +func (s *Space) Add(start address.Address, size address.Offset) { + s.free = add(s.free, start, address.Add(start, size)) +} + +// Clear removes all spaces from this space set. Used during node shutdown. +func (s *Space) Clear() { + s.free = s.free[:0] + s.ours = s.ours[:0] +} + +func (s *Space) Allocate() (bool, address.Address) { + if len(s.free) == 0 { + return false, 0 + } else { + res := s.free[0] + s.ours = add(s.ours, res, res+1) + s.free = subtract(s.free, res, res+1) + return true, res + } +} + +func (s *Space) Claim(addr address.Address) error { + if !contains(s.free, addr) { + return fmt.Errorf("Address %v is not free to claim", addr) + } + + s.ours = add(s.ours, addr, addr+1) + s.free = subtract(s.free, addr, addr+1) + return nil +} + +func (s *Space) NumFreeAddresses() address.Offset { + res := address.Offset(0) + for i := 0; i < len(s.free); i += 2 { + res += address.Subtract(s.free[i+1], s.free[i]) + } + return res +} + +func (s *Space) NumFreeAddressesInRange(start, end address.Address) address.Offset { + res := address.Offset(0) + for i := 0; i < len(s.free); i += 2 { + s, e := s.free[i], s.free[i+1] + if s < start { + s = start + } + if e > end { + e = end + } + if s >= e { + continue + } + res += address.Subtract(e, s) + } + return res +} + +func (s *Space) Free(addr address.Address) error { + if !contains(s.ours, addr) { + return fmt.Errorf("Address %v is not ours", addr) + } + if contains(s.free, addr) { + return fmt.Errorf("Address %v is already free", addr) + } + + s.ours = subtract(s.ours, addr, addr+1) + s.free = add(s.free, addr, addr+1) + return nil +} + +func (s *Space) biggestFreeRange() (int, address.Offset) { + pos := -1 + biggest := address.Offset(0) + + for i := 0; i < len(s.free); i += 2 { + size := address.Subtract(s.free[i+1], s.free[i]) + if size >= biggest { + pos = i + biggest = size + } + } + return pos, biggest +} + +func (s *Space) Donate() (address.Address, address.Offset, bool) { + if len(s.free) == 0 { + return 0, 0, false + } + + pos, biggest := s.biggestFreeRange() + + // Donate half of that biggest free range, rounding up so + // that the donation can't be empty + end := s.free[pos+1] + start := end - address.Address((biggest+1)/2) + + s.ours = subtract(s.ours, start, end) + s.free = subtract(s.free, start, end) + return start, address.Subtract(end, start), true +} + +func firstGreater(a []address.Address, x address.Address) int { + return sort.Search(len(a), func(i int) bool { return a[i] > x }) +} + +func firstGreaterOrEq(a []address.Address, x address.Address) int { + return sort.Search(len(a), func(i int) bool { return a[i] >= x }) +} + +// Do the ranges contain the given address? +func contains(addrs []address.Address, addr address.Address) bool { + return firstGreater(addrs, addr)&1 != 0 +} + +// Take the union of the range [start, end) with the ranges in the array +func add(addrs []address.Address, start address.Address, end address.Address) []address.Address { + return addSub(addrs, start, end, 0) +} + +// Subtract the range [start, end) from the ranges in the array +func subtract(addrs []address.Address, start address.Address, end address.Address) []address.Address { + return addSub(addrs, start, end, 1) +} + +func addSub(addrs []address.Address, start address.Address, end address.Address, sense int) []address.Address { + start_pos := firstGreaterOrEq(addrs, start) + end_pos := firstGreater(addrs[start_pos:], end) + start_pos + + // Boundaries up to start_pos are unaffected + res := make([]address.Address, start_pos, len(addrs)+2) + copy(res, addrs) + + // Include start and end as new boundaries if they lie + // outside/inside existing ranges (according to sense). + if start_pos&1 == sense { + res = append(res, start) + } + + if end_pos&1 == sense { + res = append(res, end) + } + + // Boundaries after end_pos are unaffected + return append(res, addrs[end_pos:]...) +} + +func (s *Space) String() string { + var buf bytes.Buffer + if len(s.ours) > 0 { + fmt.Fprint(&buf, "owned:") + for i := 0; i < len(s.ours); i += 2 { + fmt.Fprintf(&buf, " %s+%d ", s.ours[i], s.ours[i+1]-s.ours[i]) + } + } + if len(s.free) > 0 { + fmt.Fprintf(&buf, "free:") + for i := 0; i < len(s.free); i += 2 { + fmt.Fprintf(&buf, " %s+%d ", s.free[i], s.free[i+1]-s.free[i]) + } + } + if len(s.ours) == 0 && len(s.free) == 0 { + fmt.Fprintf(&buf, "No address ranges owned") + } + return buf.String() +} + +type addressSlice []address.Address + +func (p addressSlice) Len() int { return len(p) } +func (p addressSlice) Less(i, j int) bool { return p[i] < p[j] } +func (p addressSlice) Swap(i, j int) { p[i], p[j] = p[j], p[i] } + +func (s *Space) assertInvariants() { + common.Assert(sort.IsSorted(addressSlice(s.ours))) + common.Assert(sort.IsSorted(addressSlice(s.free))) +} + +// Return a slice representing everything we own, whether it is free or not +func (s *Space) everything() []address.Address { + a := make([]address.Address, len(s.ours)) + copy(a, s.ours) + for i := 0; i < len(s.free); i += 2 { + a = add(a, s.free[i], s.free[i+1]) + } + return a +} + +// OwnedRanges returns slice of Ranges, ordered by IP, gluing together +// contiguous sequences of owned and free addresses +func (s *Space) OwnedRanges() []address.Range { + everything := s.everything() + result := make([]address.Range, len(everything)/2) + for i := 0; i < len(everything); i += 2 { + result[i/2] = address.Range{Start: everything[i], End: everything[i+1]} + } + return result +} + +// Create a Space that has free space in all the supplied Ranges. +func (s *Space) AddRanges(ranges []address.Range) { + for _, r := range ranges { + s.free = add(s.free, r.Start, r.End) + } +} + +// Taking ranges to be a set of all space we should own, add in any excess as free space +func (s *Space) UpdateRanges(ranges []address.Range) { + new := []address.Address{} + for _, r := range ranges { + new = add(new, r.Start, r.End) + } + current := s.everything() + for i := 0; i < len(current); i += 2 { + new = subtract(new, current[i], current[i+1]) + } + for i := 0; i < len(new); i += 2 { + s.free = add(s.free, new[i], new[i+1]) + } +} diff --git a/ipam/space/space_test.go b/ipam/space/space_test.go new file mode 100644 index 00000000..fd13c42c --- /dev/null +++ b/ipam/space/space_test.go @@ -0,0 +1,249 @@ +package space + +import ( + "reflect" + "testing" + + "github.com/weaveworks/weave/ipam/address" + wt "github.com/weaveworks/weave/testing" +) + +func makeSpace(start address.Address, size address.Offset) *Space { + s := New() + s.Add(start, size) + return s +} + +func ip(s string) address.Address { + addr, _ := address.ParseIP(s) + return addr +} + +func TestLowlevel(t *testing.T) { + a := []address.Address{} + a = add(a, 100, 200) + wt.AssertEquals(t, a, []address.Address{100, 200}) + wt.AssertTrue(t, !contains(a, 99), "") + wt.AssertTrue(t, contains(a, 100), "") + wt.AssertTrue(t, contains(a, 199), "") + wt.AssertTrue(t, !contains(a, 200), "") + a = add(a, 700, 800) + wt.AssertEquals(t, a, []address.Address{100, 200, 700, 800}) + a = add(a, 300, 400) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 400, 700, 800}) + a = add(a, 400, 500) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 500, 700, 800}) + a = add(a, 600, 700) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 500, 600, 800}) + a = add(a, 500, 600) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 800}) + a = subtract(a, 500, 600) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 500, 600, 800}) + a = subtract(a, 600, 700) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 500, 700, 800}) + a = subtract(a, 400, 500) + wt.AssertEquals(t, a, []address.Address{100, 200, 300, 400, 700, 800}) + a = subtract(a, 300, 400) + wt.AssertEquals(t, a, []address.Address{100, 200, 700, 800}) + a = subtract(a, 700, 800) + wt.AssertEquals(t, a, []address.Address{100, 200}) + a = subtract(a, 100, 200) + wt.AssertEquals(t, a, []address.Address{}) + + s := New() + wt.AssertEquals(t, s.NumFreeAddresses(), address.Offset(0)) + ok, got := s.Allocate() + wt.AssertFalse(t, ok, "allocate in empty space should fail") + + s.Add(100, 100) + wt.AssertEquals(t, s.NumFreeAddresses(), address.Offset(100)) + ok, got = s.Allocate() + wt.AssertTrue(t, ok && got == 100, "allocate") + wt.AssertEquals(t, s.NumFreeAddresses(), address.Offset(99)) + wt.AssertNoErr(t, s.Claim(150)) + wt.AssertEquals(t, s.NumFreeAddresses(), address.Offset(98)) + wt.AssertNoErr(t, s.Free(100)) + wt.AssertEquals(t, s.NumFreeAddresses(), address.Offset(99)) + wt.AssertErrorInterface(t, s.Free(0), (*error)(nil), "free not allocated") + wt.AssertErrorInterface(t, s.Free(100), (*error)(nil), "double free") + + start, size, ok := s.Donate() + wt.AssertTrue(t, ok && start == 125 && size == 25, "donate") + + // test Donate when addresses are scarce + s = New() + start, size, ok = s.Donate() + wt.AssertTrue(t, !ok, "donate on empty space should fail") + s.Add(0, 3) + wt.AssertNoErr(t, s.Claim(0)) + wt.AssertNoErr(t, s.Claim(2)) + start, size, ok = s.Donate() + wt.AssertTrue(t, ok && start == 1 && size == 1, "donate") + start, size, ok = s.Donate() + wt.AssertTrue(t, !ok, "donate should fail") +} + +func TestSpaceAllocate(t *testing.T) { + const ( + testAddr1 = "10.0.3.4" + testAddrx = "10.0.3.19" + testAddry = "10.0.9.19" + containerID = "deadbeef" + ) + + space1 := makeSpace(ip(testAddr1), 20) + wt.AssertEquals(t, space1.NumFreeAddresses(), address.Offset(20)) + space1.assertInvariants() + + _, addr1 := space1.Allocate() + wt.AssertEqualString(t, addr1.String(), testAddr1, "address") + wt.AssertEquals(t, space1.NumFreeAddresses(), address.Offset(19)) + space1.assertInvariants() + + _, addr2 := space1.Allocate() + wt.AssertNotEqualString(t, addr2.String(), testAddr1, "address") + wt.AssertEquals(t, space1.NumFreeAddresses(), address.Offset(18)) + wt.AssertEquals(t, space1.NumFreeAddressesInRange(ip(testAddr1), ip(testAddrx)), address.Offset(13)) + wt.AssertEquals(t, space1.NumFreeAddressesInRange(ip(testAddr1), ip(testAddry)), address.Offset(18)) + space1.assertInvariants() + + space1.Free(addr2) + space1.assertInvariants() + + wt.AssertErrorInterface(t, space1.Free(addr2), (*error)(nil), "double free") + wt.AssertErrorInterface(t, space1.Free(ip(testAddrx)), (*error)(nil), "address not allocated") + wt.AssertErrorInterface(t, space1.Free(ip(testAddry)), (*error)(nil), "wrong out of range") + + space1.assertInvariants() +} + +func TestSpaceFree(t *testing.T) { + const ( + testAddr1 = "10.0.3.4" + testAddrx = "10.0.3.19" + testAddry = "10.0.9.19" + containerID = "deadbeef" + ) + + space := makeSpace(ip(testAddr1), 20) + + // Check we are prepared to give up the entire space + pos, size := space.biggestFreeRange() + wt.AssertTrue(t, space.free[pos] == ip(testAddr1) && size == 20, "Wrong space") + + for i := 0; i < 20; i++ { + ok, _ := space.Allocate() + wt.AssertTrue(t, ok, "Failed to get address") + } + + // Check we are full + ok, _ := space.Allocate() + wt.AssertTrue(t, !ok, "Should have failed to get address") + _, size, ok = space.Donate() + wt.AssertTrue(t, size == 0, "Wrong space") + + // Free in the middle + wt.AssertSuccess(t, space.Free(ip("10.0.3.13"))) + pos, size = space.biggestFreeRange() + wt.AssertTrue(t, space.free[pos] == ip("10.0.3.13") && size == 1, "Wrong space") + + // Free one at the end + wt.AssertSuccess(t, space.Free(ip("10.0.3.23"))) + pos, size = space.biggestFreeRange() + wt.AssertTrue(t, space.free[pos] == ip("10.0.3.23") && size == 1, "Wrong space") + + // Now free a few at the end + wt.AssertSuccess(t, space.Free(ip("10.0.3.22"))) + wt.AssertSuccess(t, space.Free(ip("10.0.3.21"))) + + wt.AssertEquals(t, space.NumFreeAddresses(), address.Offset(4)) + + // Now get the biggest free space; should be 3.21 + pos, size = space.biggestFreeRange() + wt.AssertTrue(t, space.free[pos] == ip("10.0.3.21") && size == 3, "Wrong space") + + // Now free a few in the middle + wt.AssertSuccess(t, space.Free(ip("10.0.3.12"))) + wt.AssertSuccess(t, space.Free(ip("10.0.3.11"))) + wt.AssertSuccess(t, space.Free(ip("10.0.3.10"))) + + wt.AssertEquals(t, space.NumFreeAddresses(), address.Offset(7)) + + // Now get the biggest free space; should be 3.21 + pos, size = space.biggestFreeRange() + wt.AssertTrue(t, space.free[pos] == ip("10.0.3.10") && size == 4, "Wrong space") + + wt.AssertEquals(t, space.OwnedRanges(), []address.Range{address.Range{Start: ip("10.0.3.4"), End: ip("10.0.3.24")}}) +} + +func (s1 *Space) Equal(s2 *Space) bool { + return reflect.DeepEqual(s1.ours, s2.ours) && reflect.DeepEqual(s1.free, s2.free) +} + +func TestDonateSimple(t *testing.T) { + const ( + testAddr1 = "10.0.1.0" + testAddr2 = "10.0.1.32" + ) + + var ( + ipAddr1 = ip(testAddr1) + ) + + ps1 := makeSpace(ipAddr1, 48) + + // Empty space set should split in two and give me the second half + start, numGivenUp, ok := ps1.Donate() + wt.AssertBool(t, ok, true, "Donate result") + wt.AssertEqualString(t, start.String(), "10.0.1.24", "Invalid start") + wt.AssertEquals(t, numGivenUp, address.Offset(24)) + wt.AssertEquals(t, ps1.NumFreeAddresses(), address.Offset(24)) + + // Now check we can give the rest up. + count := 0 // count to avoid infinite loop + for ; count < 1000; count++ { + _, size, ok := ps1.Donate() + if !ok { + break + } + numGivenUp += size + } + wt.AssertEquals(t, ps1.NumFreeAddresses(), address.Offset(0)) + wt.AssertEquals(t, numGivenUp, address.Offset(48)) +} + +func TestDonateHard(t *testing.T) { + //common.InitDefaultLogging(true) + var ( + start = ip("10.0.1.0") + size address.Offset = 48 + ) + + // Fill a fresh space + spaceset := makeSpace(start, size) + for i := address.Offset(0); i < size; i++ { + ok, _ := spaceset.Allocate() + wt.AssertTrue(t, ok, "Failed to get IP!") + } + + wt.AssertEquals(t, spaceset.NumFreeAddresses(), address.Offset(0)) + + // Now free all but the last address + // this will force us to split the free list + for i := address.Offset(0); i < size-1; i++ { + wt.AssertSuccess(t, spaceset.Free(address.Add(start, i))) + } + + // Now split + newRange, numGivenUp, ok := spaceset.Donate() + wt.AssertBool(t, ok, true, "GiveUpSpace result") + wt.AssertEquals(t, newRange, ip("10.0.1.23")) + wt.AssertEquals(t, numGivenUp, address.Offset(24)) + wt.AssertEquals(t, spaceset.NumFreeAddresses(), address.Offset(23)) + + //Space set should now have 2 spaces + expected := New() + expected.Add(start, 23) + expected.ours = add(nil, ip("10.0.1.47"), ip("10.0.1.48")) + wt.AssertEquals(t, spaceset, expected) +} diff --git a/ipam/testutils_test.go b/ipam/testutils_test.go new file mode 100644 index 00000000..ee72d8af --- /dev/null +++ b/ipam/testutils_test.go @@ -0,0 +1,286 @@ +package ipam + +import ( + "fmt" + "math/rand" + "testing" + "time" + + "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/router" + wt "github.com/weaveworks/weave/testing" +) + +type mockMessage struct { + dst router.PeerName + msgType byte + buf []byte +} + +func (m *mockMessage) String() string { + return fmt.Sprintf("-> %s [%x]", m.dst, m.buf) +} + +func toStringArray(messages []mockMessage) []string { + out := make([]string, len(messages)) + for i := range out { + out[i] = messages[i].String() + } + return out +} + +type mockGossipComms struct { + t *testing.T + name string + messages []mockMessage +} + +func (m *mockGossipComms) String() string { + return fmt.Sprintf("[mockGossipComms %s]", m.name) +} + +// Note: this style of verification, using equalByteBuffer, requires +// that the contents of messages are never re-ordered. Which, for instance, +// requires they are not based off iterating through a map. + +func (m *mockGossipComms) GossipBroadcast(update router.GossipData) error { + buf := []byte{} + if len(m.messages) == 0 { + m.Fatalf("%s: Gossip broadcast message unexpected: \n%x", m.name, buf) + } else if msg := m.messages[0]; msg.dst != router.UnknownPeerName { + m.Fatalf("%s: Expected Gossip message to %s but got broadcast", m.name, msg.dst) + } else if msg.buf != nil && !equalByteBuffer(msg.buf, buf) { + m.Fatalf("%s: Gossip message not sent as expected: \nwant: %x\ngot : %x", m.name, msg.buf, buf) + } else { + // Swallow this message + m.messages = m.messages[1:] + } + return nil +} + +func equalByteBuffer(a, b []byte) bool { + if len(a) != len(b) { + return false + } + for i := 0; i < len(a); i++ { + if a[i] != b[i] { + return false + } + } + return true +} + +func (m *mockGossipComms) Fatalf(format string, args ...interface{}) { + // this sometimes hangs: wt.Fatalf(m.t, args...) + panic(fmt.Sprintf(format, args...)) +} + +func (m *mockGossipComms) GossipUnicast(dstPeerName router.PeerName, buf []byte) error { + if len(m.messages) == 0 { + m.Fatalf("%s: Gossip message to %s unexpected: \n%s", m.name, dstPeerName, buf) + } else if msg := m.messages[0]; msg.dst == router.UnknownPeerName { + m.Fatalf("%s: Expected Gossip broadcast message but got dest %s", m.name, dstPeerName) + } else if msg.dst != dstPeerName { + m.Fatalf("%s: Expected Gossip message to %s but got dest %s", m.name, msg.dst, dstPeerName) + } else if buf[0] != msg.msgType { + m.Fatalf("%s: Expected Gossip message of type %d but got type %d", m.name, msg.msgType, buf[0]) + } else if msg.buf != nil && !equalByteBuffer(msg.buf, buf[1:]) { + m.Fatalf("%s: Gossip message not sent as expected: \nwant: %x\ngot : %x", m.name, msg.buf, buf[1:]) + } else { + // Swallow this message + m.messages = m.messages[1:] + } + return nil +} + +func ExpectMessage(alloc *Allocator, dst string, msgType byte, buf []byte) { + m := alloc.gossip.(*mockGossipComms) + dstPeerName, _ := router.PeerNameFromString(dst) + m.messages = append(m.messages, mockMessage{dstPeerName, msgType, buf}) +} + +func ExpectBroadcastMessage(alloc *Allocator, buf []byte) { + m := alloc.gossip.(*mockGossipComms) + m.messages = append(m.messages, mockMessage{router.UnknownPeerName, 0, buf}) +} + +func CheckAllExpectedMessagesSent(allocs ...*Allocator) { + for _, alloc := range allocs { + m := alloc.gossip.(*mockGossipComms) + if len(m.messages) > 0 { + wt.Fatalf(m.t, "%s: Gossip message(s) not sent as expected: \n%x", m.name, m.messages) + } + } +} + +func makeAllocator(name string, cidr string, quorum uint) *Allocator { + peername, err := router.PeerNameFromString(name) + if err != nil { + panic(err) + } + + alloc, err := NewAllocator(peername, router.PeerUID(rand.Int63()), + "nick-"+name, cidr, quorum) + if err != nil { + panic(err) + } + + return alloc +} + +func makeAllocatorWithMockGossip(t *testing.T, name string, universeCIDR string, quorum uint) *Allocator { + alloc := makeAllocator(name, universeCIDR, quorum) + gossip := &mockGossipComms{t: t, name: name} + alloc.SetInterfaces(gossip) + alloc.Start() + return alloc +} + +func (alloc *Allocator) claimRingForTesting(allocs ...*Allocator) { + peers := []router.PeerName{alloc.ourName} + for _, alloc2 := range allocs { + peers = append(peers, alloc2.ourName) + } + alloc.ring.ClaimForPeers(normalizeConsensus(peers)) + alloc.space.AddRanges(alloc.ring.OwnedRanges()) +} + +// Check whether or not something was sent on a channel +func AssertSent(t *testing.T, ch <-chan bool) { + timeout := time.After(10 * time.Second) + select { + case <-ch: + // This case is ok + case <-timeout: + wt.Fatalf(t, "Nothing sent on channel") + } +} + +func AssertNothingSent(t *testing.T, ch <-chan bool) { + select { + case val := <-ch: + wt.Fatalf(t, "Unexpected value on channel: %t", val) + default: + // no message received + } +} + +func AssertNothingSentErr(t *testing.T, ch <-chan error) { + select { + case val := <-ch: + wt.Fatalf(t, "Unexpected value on channel: %t", val) + default: + // no message received + } +} + +// Router to convey gossip from one gossiper to another, for testing +type gossipMessage struct { + isUnicast bool + sender *router.PeerName + buf []byte + exitChan chan bool +} + +type TestGossipRouter struct { + gossipChans map[router.PeerName]chan gossipMessage + loss float32 // 0.0 means no loss +} + +func (grouter *TestGossipRouter) GossipBroadcast(update router.GossipData) error { + for _, gossipChan := range grouter.gossipChans { + select { + case gossipChan <- gossipMessage{buf: update.(*ipamGossipData).alloc.encode()}: + default: // drop the message if we cannot send it + } + } + return nil +} + +func (grouter *TestGossipRouter) removePeer(peer router.PeerName) { + gossipChan := grouter.gossipChans[peer] + resultChan := make(chan bool) + gossipChan <- gossipMessage{exitChan: resultChan} + <-resultChan + delete(grouter.gossipChans, peer) +} + +type TestGossipRouterClient struct { + router *TestGossipRouter + sender router.PeerName +} + +func (grouter *TestGossipRouter) connect(sender router.PeerName, gossiper router.Gossiper) router.Gossip { + gossipChan := make(chan gossipMessage, 100) + + go func() { + gossipTimer := time.Tick(10 * time.Second) + for { + select { + case message := <-gossipChan: + if message.exitChan != nil { + message.exitChan <- true + return + } + + if rand.Float32() > (1.0 - grouter.loss) { + continue + } + + if message.isUnicast { + err := gossiper.OnGossipUnicast(*message.sender, message.buf) + if err != nil { + panic(fmt.Sprintf("Error doing gossip unicast to %s: %s", sender, err)) + } + } else { + _, err := gossiper.OnGossipBroadcast(message.buf) + if err != nil { + panic(fmt.Sprintf("Error doing gossip broadcast to %s: %s", sender, err)) + } + } + case <-gossipTimer: + grouter.GossipBroadcast(gossiper.Gossip()) + } + } + }() + + grouter.gossipChans[sender] = gossipChan + return TestGossipRouterClient{grouter, sender} +} + +func (client TestGossipRouterClient) GossipUnicast(dstPeerName router.PeerName, buf []byte) error { + select { + case client.router.gossipChans[dstPeerName] <- gossipMessage{isUnicast: true, sender: &client.sender, buf: buf}: + default: // drop the message if we cannot send it + common.Error.Printf("Dropping message") + } + return nil +} + +func (client TestGossipRouterClient) GossipBroadcast(update router.GossipData) error { + return client.router.GossipBroadcast(update) +} + +func makeNetworkOfAllocators(size int, cidr string) ([]*Allocator, TestGossipRouter) { + + gossipRouter := TestGossipRouter{make(map[router.PeerName]chan gossipMessage), 0.0} + allocs := make([]*Allocator, size) + + for i := 0; i < size; i++ { + alloc := makeAllocator(fmt.Sprintf("%02d:00:00:02:00:00", i), + cidr, uint(size/2+1)) + alloc.SetInterfaces(gossipRouter.connect(alloc.ourName, alloc)) + alloc.Start() + allocs[i] = alloc + } + + gossipRouter.GossipBroadcast(allocs[size-1].Gossip()) + time.Sleep(1000 * time.Millisecond) + return allocs, gossipRouter +} + +func stopNetworkOfAllocators(allocs []*Allocator) { + for _, alloc := range allocs { + alloc.Stop() + } +} diff --git a/ipam/utils/utils.go b/ipam/utils/utils.go deleted file mode 100644 index 18957155..00000000 --- a/ipam/utils/utils.go +++ /dev/null @@ -1,49 +0,0 @@ -package utils - -import ( - "net" -) - -// We shouldn't ever get any errors on *encoding*, but if we do, this will make sure we get to hear about them. -func panicOnError(err error) { - if err != nil { - panic(err) - } -} - -// IP4int converts an ipv4 address to a uint32 -func IP4int(ip4 net.IP) (r uint32) { - for _, b := range ip4.To4() { - r <<= 8 - r |= uint32(b) - } - return -} - -// IntIP4 converts a uint32 to an ipv4 address -func IntIP4(key uint32) (r net.IP) { - r = make([]byte, net.IPv4len) - for i := 3; i >= 0; i-- { - r[i] = byte(key) - key >>= 8 - } - return -} - -// Add - convert to 32-bit unsigned integer, add, and convert back -func Add(addr net.IP, i uint32) net.IP { - sum := IP4int(addr) + i - return IntIP4(sum) -} - -// Subtract - convert to 32-bit unsigned integer, subtract, and convert back -func Subtract(a, b net.IP) int64 { - return int64(IP4int(a)) - int64(IP4int(b)) -} - -// Assert test is true, panic otherwise -func Assert(test bool, message string) { - if !test { - panic(message) - } -} diff --git a/router/protocol.go b/router/protocol.go index 441c208c..41f0f70c 100644 --- a/router/protocol.go +++ b/router/protocol.go @@ -2,7 +2,7 @@ package router const ( Protocol = "weave" - ProtocolVersion = 16 + ProtocolVersion = 102 ) type ProtocolTag byte diff --git a/site/features.md b/site/features.md index 11ec05bb..3b75a80d 100644 --- a/site/features.md +++ b/site/features.md @@ -9,6 +9,7 @@ Weave has a few more features beyond those illustrated by the [basic example](https://github.com/weaveworks/weave#example): * [Virtual ethernet switch](#virtual-ethernet-switch) + * [IP Address Management](#ipam) * [Application isolation](#application-isolation) * [Dynamic network attachment](#dynamic-network-attachment) * [Security](#security) @@ -53,6 +54,53 @@ now re-use the same tools and techniques when deploying applications as containers as we would have done when deploying them 'on metal' in our data centre. +### IP Address Management + +Weave can assign unique IP addresses to each container across the +network. To make this work, weave must be told what range of +addresses to allocate from, for example: + + host1# weave launch -iprange 10.2.3.0/24 + +The `run`, `start`, `attach`, `expose` and `hide` commands will then fetch +an address automatically if none is specified, i.e.: + + host1# D=$(weave run -t -i ubuntu) + +Weave will automatically learn when a container has exited +and hence can release its IP address. + +Weave shares the IP range across all peers, dynamically according to +their needs. If a group of peers becomes isolated from the rest (a +partition), they can continue to work with the IP ranges they had +before isolation, and can be re-connected to the rest of the network +and carry on. Note that you must specify the same range with +`-iprange` on each host, and you cannot mix weaves started with and +without -iprange. + +Just once, when you start up the whole network, weave needs a majority +of peers to agree in order to avoid isolated groups starting off +inconsistently. Therefore, you must either supply the list of all +peers in the network to `weave launch` or add the `-initpeercount` +flag to specify how many peers there will be. It isn't a problem to +over-estimate by a bit, but if you supply a number that is too small +then multiple independent groups may form. + +You may wish to `weave stop` and re-launch to change some config or to +upgrade to a new version; provided the underlying protocol hasn't +changed it will pick up where it left off and learn from peers in the +network which address ranges it was previously using. If, however, you +run `weave reset` this will remove the peer from the network so +if Weave is run again on that node it will start from scratch. + +For failed peers, the `weave rmpeer` command can be used to +permanently remove the ranges allocated to said peer. This will allow +other peers to allocate IPs in the ranges previously owner by the rm'd +peer, and as such should be used with extreme caution - if the rm'd +peer had transferred some range of IP addresses to another peer but +this is not known to the whole network, or if it later rejoins +the Weave network, the same IP address may be allocated twice. + ### Application isolation A single weave network can host multiple, isolated applications, with diff --git a/test/120_ipam_test.sh b/test/120_ipam_test.sh new file mode 100755 index 00000000..f7cb15d1 --- /dev/null +++ b/test/120_ipam_test.sh @@ -0,0 +1,18 @@ +#! /bin/bash + +. ./config.sh + +UNIVERSE=10.2.1.0/24 + +start_suite "Ping over cross-host weave network with IPAM" + +weave_on $HOST1 launch -iprange $UNIVERSE +weave_on $HOST2 launch -iprange $UNIVERSE $HOST1 + +weave_on $HOST1 run -t --name=c1 gliderlabs/alpine /bin/sh +weave_on $HOST2 run -t --name=c2 gliderlabs/alpine /bin/sh +# Note can't use weave_on here because it echoes the command +C2IP=$(DOCKER_HOST=tcp://$HOST2:2375 $WEAVE ps | grep -o -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') +assert_raises "exec_on $HOST1 c1 ping -q -c 4 $C2IP" + +end_suite diff --git a/test/140_ipam_expose_test.sh b/test/140_ipam_expose_test.sh new file mode 100755 index 00000000..01f74df5 --- /dev/null +++ b/test/140_ipam_expose_test.sh @@ -0,0 +1,63 @@ +#! /bin/bash + +. ./config.sh + +R1=10.2.1.0/24 +C3=10.2.2.34 +C4=10.2.2.37 + +PING="ping -nq -W 1 -c 1" + +weave_on1() { + assert_raises "weave_on $HOST1 $@" +} + +run_on1() { + assert_raises "run_on $HOST1 $@" +} + +exec_on1() { + assert_raises "exec_on $HOST1 $@" +} + +check_container_connectivity() { + exec_on1 "c2 $PING $C1" + exec_on1 "c3 $PING $C4" + # fails due to #620 + # exec_on1 "c3 ! $PING $C1" +} + +start_suite "exposing weave network to host" + +weave_on $HOST1 launch -iprange $R1 + +weave_on $HOST1 run -t --name=c1 gliderlabs/alpine /bin/sh +weave_on $HOST1 run -t --name=c2 gliderlabs/alpine /bin/sh +weave_on $HOST1 run $C3/24 -t --name=c3 gliderlabs/alpine /bin/sh +weave_on $HOST1 run $C4/24 -t --name=c4 gliderlabs/alpine /bin/sh + +# Note can't use weave_on here because it echoes the command +C1=$(DOCKER_HOST=tcp://$HOST1:2375 $WEAVE ps c1 | grep -o -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') + +# absence of host connectivity by default +run_on1 "! $PING $C1" +check_container_connectivity + +# host connectivity after 'expose' +weave_on1 "expose" +run_on1 " $PING $C1" +run_on1 "! $PING $C3" +check_container_connectivity + +# idempotence of 'expose' +weave_on1 "expose" +run_on1 " $PING $C1" + +# no host connectivity after 'hide' +weave_on1 "hide" +run_on1 "! $PING $C1" + +# idempotence of 'hide' +weave_on1 "hide" + +end_suite diff --git a/test/250_dns_ipam_test.sh b/test/250_dns_ipam_test.sh new file mode 100755 index 00000000..07bc302c --- /dev/null +++ b/test/250_dns_ipam_test.sh @@ -0,0 +1,27 @@ +#! /bin/bash + +. ./config.sh + +UNIVERSE=10.2.3.0/24 + +start_suite "Resolve names over cross-host weave network with IPAM" + +weave_on $HOST1 launch -iprange $UNIVERSE +weave_on $HOST2 launch -iprange $UNIVERSE $HOST1 + +weave_on $HOST1 launch-dns 10.2.254.1/24 +weave_on $HOST2 launch-dns 10.2.254.2/24 + +weave_on $HOST2 run -t --name=c2 -h seetwo.weave.local gliderlabs/alpine /bin/sh +weave_on $HOST1 run --with-dns --name=c1 -t aanand/docker-dnsutils /bin/sh + +# Note can't use weave_on here because it echoes the command +C2IP=$(DOCKER_HOST=tcp://$HOST2:2375 $WEAVE ps c2 | grep -o -E '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}') + +ok=$(docker -H tcp://$HOST1:2375 exec -i c1 dig +short seetwo.weave.local) +assert "echo $ok" "$C2IP" + +ok=$(docker -H tcp://$HOST1:2375 exec -i c1 dig +short -x $C2IP) +assert "echo $ok" "seetwo.weave.local." + +end_suite diff --git a/weave b/weave index b85d6200..055ef756 100755 --- a/weave +++ b/weave @@ -25,27 +25,29 @@ CIDR_REGEXP="$IP_REGEXP/[0-9]{1,2}" usage() { echo "Usage:" echo "weave setup" - echo "weave launch [-password ] [-nickname ] ..." + echo "weave launch [-password ] [-nickname ] [-iprange ] ..." echo "weave launch-dns " echo "weave connect " echo "weave forget " - echo "weave run [--with-dns] [ ...] ..." - echo "weave start [ ...] " - echo "weave attach [ ...] " + echo "weave run [--with-dns] [ ...] ..." + echo "weave start [ ...] " + echo "weave attach [ ...] " echo "weave detach [ ...] " echo "weave dns-add [ ...] [-h ]" echo "weave dns-remove [ ...] " - echo "weave expose [ ...] [-h ]" - echo "weave hide [ ...]" + echo "weave expose [ ...] [-h ]" + echo "weave hide [ ...]" echo "weave ps [ ...]" echo "weave status" echo "weave version" echo "weave stop" echo "weave stop-dns" echo "weave reset" + echo "weave rmpeer " echo echo "where is of the form [:], and" echo " is of the form /" + echo " is a nickname or weave internal peer ID" exit 1 } @@ -98,9 +100,6 @@ collect_cidr_args() { CIDR_COUNT=$((CIDR_COUNT + 1)) shift 1 done - if [ $CIDR_COUNT -eq 0 ]; then - [ $# -gt 0 ] && validate_cidr "$1" || usage - fi } is_ip() { @@ -532,6 +531,28 @@ wait_for_status() { return 1 } +# Call $1 for all containers, passing container ID, all MACs and all IPs +with_container_addresses() { + COMMAND=$1 + shift 1 + for CONTAINER_ID in "$@" ; do + if [ "$CONTAINER_ID" = "weave:expose" ]; then + ADDRS_CMD="ip addr show dev $BRIDGE" + else + ADDRS_CMD="with_container_netns $CONTAINER_ID container_weave_addrs" + fi + if CONTAINER_ADDRS=$($ADDRS_CMD 2>&1) ; then + CONTAINER_MAC=$(echo "$CONTAINER_ADDRS" | grep -o 'link/ether .*' | cut -d ' ' -f 2) + CONTAINER_IPS=$(echo "$CONTAINER_ADDRS" | grep -o 'inet .*' | cut -d ' ' -f 2) + $COMMAND "$CONTAINER_ID" "$CONTAINER_MAC" "$CONTAINER_IPS" + fi + done +} + +echo_addresses() { + echo $1 $2 $3 +} + ###################################################################### # weaveDNS helpers ###################################################################### @@ -614,6 +635,53 @@ populate_dns() { done } +###################################################################### +# IP Allocation Management helpers +###################################################################### + +populate_1_ipam() { + for CIDR in $3; do + # NB: CONTAINER_IP is the IP of the weave container; it is set by wait_for_status. + http_call_ip $CONTAINER_IP $HTTP_PORT PUT /ip/$1/${CIDR%/*} + done +} + +# Tell the newly-started weave IP allocator about existing weave IPs +populate_ipam() { + wait_for_status $CONTAINER_NAME $HTTP_PORT + with_container_addresses populate_1_ipam weave:expose $(docker ps -q) +} + +# If we don't already have any CIDRs passed from the command-line, get one from IPAM +# $1 is the name of the container and $2, if set, is the ID to use inside IPAM +get_ipam_cidr() { + if [ -z $CIDR_ARGS ] ; then + if [ -z $2 ] ; then + # get the long form of the container ID + if ! status=$(docker inspect --format='{{.State.Running}} {{.Id}}' $1 2>/dev/null); then + echo "Error: No such container: $1" >&2 + return 1 + fi + case "$status" in + true*) + CONTAINER="${status#true }" + ;; + *) + echo "$1 container is not running." >&2 + return 1 + ;; + esac + else + CONTAINER="$2" + fi + CIDR_ARGS="$(http_call $CONTAINER_NAME $HTTP_PORT POST /ip/$CONTAINER)" + if [ "$CIDR_ARGS" = "404 page not found" ] ; then + echo "No IP address supplied (use the -iprange option on 'weave launch'\nto enable IP allocation)" >&2 + return 1 + fi + fi +} + ########################################################################################## [ $(id -u) = 0 ] || { @@ -697,6 +765,12 @@ case "$COMMAND" in CONTAINER_PORT="$2" shift 2 ;; + -iprange) + [ $# -gt 1 ] || usage + validate_cidr $2 + IPRANGE="-iprange $2" + shift 2 + ;; *) break ;; @@ -706,10 +780,10 @@ case "$COMMAND" in # additional parameters, such as resource limits, to docker # when launching the weave container. CONTAINER=$(docker run --privileged -d --name=$CONTAINER_NAME \ - -p $PORT:$CONTAINER_PORT/tcp -p $PORT:$CONTAINER_PORT/udp -e WEAVE_PASSWORD \ - $WEAVE_DOCKER_ARGS $IMAGE -iface $CONTAINER_IFNAME -port $CONTAINER_PORT -name "$PEERNAME" -nickname "$(hostname)" "$@") + -p $PORT:$CONTAINER_PORT/tcp -p $PORT:$CONTAINER_PORT/udp -e WEAVE_PASSWORD -v /var/run/docker.sock:/var/run/docker.sock \ + $WEAVE_DOCKER_ARGS $IMAGE -iface $CONTAINER_IFNAME -port $CONTAINER_PORT -name "$PEERNAME" -nickname "$(hostname)" $IPRANGE "$@") with_container_netns $CONTAINER launch >/dev/null - wait_for_status $CONTAINER_NAME $HTTP_PORT + [ -z "$IPRANGE" ] && wait_for_status $CONTAINER_NAME $HTTP_PORT || populate_ipam echo $CONTAINER ;; launch-dns) @@ -745,18 +819,7 @@ case "$COMMAND" in ;; ps) [ $# -eq 0 ] && CONTAINERS="weave:expose $(docker ps -q)" || CONTAINERS="$@" - for CONTAINER_ID in $CONTAINERS ; do - if [ "$CONTAINER_ID" = "weave:expose" ]; then - ADDRS_CMD="ip addr show dev $BRIDGE" - else - ADDRS_CMD="with_container_netns $CONTAINER_ID container_weave_addrs" - fi - if CONTAINER_ADDRS=$($ADDRS_CMD 2>&1) ; then - CONTAINER_MAC=$(echo "$CONTAINER_ADDRS" | grep -o 'link/ether .*' | cut -d ' ' -f 2) - CONTAINER_IPS=$(echo "$CONTAINER_ADDRS" | grep -o 'inet .*' | cut -d ' ' -f 2) - echo $CONTAINER_ID $CONTAINER_MAC $CONTAINER_IPS - fi - done + with_container_addresses echo_addresses $CONTAINERS ;; version) [ $# -eq 0 ] || usage @@ -799,6 +862,7 @@ case "$COMMAND" in CONTAINER_ID="$1" create_bridge CONTAINER=$(docker start $CONTAINER_ID) + get_ipam_cidr $CONTAINER_ID $CONTAINER with_container_netns $CONTAINER_ID attach $CIDR_ARGS >/dev/null tell_dns PUT $CONTAINER_ID $CIDR_ARGS echo $CONTAINER @@ -809,6 +873,7 @@ case "$COMMAND" in [ $# -eq 1 ] || usage CONTAINER_ID="$1" create_bridge + get_ipam_cidr $CONTAINER_ID with_container_netns $CONTAINER_ID attach $CIDR_ARGS >/dev/null tell_dns PUT $CONTAINER_ID $CIDR_ARGS ;; @@ -843,6 +908,7 @@ case "$COMMAND" in expose) collect_cidr_args "$@" shift $CIDR_COUNT + [ $CIDR_COUNT -gt 0 ] || get_ipam_cidr weave weave:expose if [ $# -eq 0 ]; then FQDN="" else @@ -866,7 +932,7 @@ case "$COMMAND" in hide) collect_cidr_args "$@" shift $CIDR_COUNT - [ $# -eq 0 ] || usage + get_ipam_cidr $CONTAINER_NAME weave:expose create_bridge --without-ethtool for CIDR in $CIDR_ARGS; do if ip addr show dev $BRIDGE | grep -qF $CIDR @@ -877,6 +943,7 @@ case "$COMMAND" in http_call $DNS_CONTAINER_NAME $DNS_HTTP_PORT DELETE /name/weave:expose/${CIDR%/*} 2>/dev/null || true fi done + http_call $CONTAINER_NAME $HTTP_PORT DELETE /ip/weave:expose > /dev/null && true ;; stop) [ $# -eq 0 ] || usage @@ -895,6 +962,7 @@ case "$COMMAND" in ;; reset) [ $# -eq 0 ] || usage + http_call $CONTAINER_NAME $HTTP_PORT DELETE /peer >/dev/null 2>&1 || true docker stop $CONTAINER_NAME >/dev/null 2>&1 || true docker stop $DNS_CONTAINER_NAME >/dev/null 2>&1 || true docker rm -f $CONTAINER_NAME >/dev/null 2>&1 || true @@ -905,6 +973,11 @@ case "$COMMAND" in ip link del $LOCAL_IFNAME done ;; + rmpeer) + [ $# -eq 1 ] || usage + PEER=$1 + http_call $CONTAINER_NAME $HTTP_PORT DELETE /peer/$PEER + ;; *) echo "Unknown weave command '$COMMAND'" >&2 usage diff --git a/weaver/main.go b/weaver/main.go index 4ee8ef26..85e5e8b8 100644 --- a/weaver/main.go +++ b/weaver/main.go @@ -8,6 +8,8 @@ import ( "github.com/davecheney/profile" "github.com/gorilla/mux" . "github.com/weaveworks/weave/common" + "github.com/weaveworks/weave/common/updater" + "github.com/weaveworks/weave/ipam" weavenet "github.com/weaveworks/weave/net" weave "github.com/weaveworks/weave/router" "log" @@ -46,6 +48,9 @@ func main() { peers []string bufSzMB int httpAddr string + iprangeCIDR string + peerCount int + apiPath string ) flag.BoolVar(&justVersion, "version", false, "print version and exit") @@ -60,6 +65,9 @@ func main() { flag.IntVar(&config.ConnLimit, "connlimit", 30, "connection limit (0 for unlimited)") flag.IntVar(&bufSzMB, "bufsz", 8, "capture buffer size in MB") flag.StringVar(&httpAddr, "httpaddr", fmt.Sprintf(":%d", weave.HTTPPort), "address to bind HTTP interface to (disabled if blank, absolute path indicates unix domain socket)") + flag.StringVar(&iprangeCIDR, "iprange", "", "IP address range to allocate within, in CIDR notation") + flag.IntVar(&peerCount, "initpeercount", 0, "number of peers in network (for IP address allocation)") + flag.StringVar(&apiPath, "api", "unix:///var/run/docker.sock", "Path to Docker API socket") flag.Parse() peers = flag.Args() @@ -121,12 +129,23 @@ func main() { router := weave.NewRouter(config, name, nickName) log.Println("Our name is", router.Ourself) - router.Start() - initiateConnections(router, peers) + var allocator *ipam.Allocator if httpAddr != "" { - go handleHTTP(router, httpAddr) + if iprangeCIDR != "" { + allocator = createAllocator(router, apiPath, iprangeCIDR, determineQuorum(peerCount, peers)) + } else { + if peerCount > 0 { + log.Fatal("-initpeercount flag specified without -iprange") + } + } + } + if httpAddr == "" || iprangeCIDR == "" { + router.NewGossip("IPallocation", &ipam.DummyAllocator{}) } + router.Start() + initiateConnections(router, peers) + go handleHTTP(router, httpAddr, allocator) SignalHandlerLoop(router) } @@ -164,7 +183,41 @@ func initiateConnections(router *weave.Router, peers []string) { } } -func handleHTTP(router *weave.Router, httpAddr string) { +func createAllocator(router *weave.Router, apiPath string, iprangeCIDR string, quorum uint) *ipam.Allocator { + allocator, err := ipam.NewAllocator(router.Ourself.Peer.Name, router.Ourself.Peer.UID, router.Ourself.Peer.NickName, iprangeCIDR, quorum) + if err != nil { + log.Fatal(err) + } + allocator.SetInterfaces(router.NewGossip("IPallocation", allocator)) + allocator.Start() + err = updater.Start(apiPath, allocator) + if err != nil { + log.Fatal("Unable to start watcher", err) + } + return allocator +} + +// Pick a quorum size heuristically based on the number of peer +// addresses passed. +func determineQuorum(initPeerCountFlag int, peers []string) uint { + if initPeerCountFlag > 0 { + return uint(initPeerCountFlag/2 + 1) + } + + // Guess a suitable quorum size based on the list of peer + // addresses. The peer list might or might not contain an + // address for this peer, so the conservative assumption is + // that it doesn't. The list might contain multiple addresses + // that resolve to the same peer, in which case the quorum + // might be larger than it needs to be. But the user can + // specify it explicitly if that becomes a problem. + clusterSize := uint(len(peers) + 1) + quorum := clusterSize/2 + 1 + log.Println("Assuming quorum size of", quorum) + return quorum +} + +func handleHTTP(router *weave.Router, httpAddr string, allocator *ipam.Allocator) { encryption := "off" if router.UsingPassword() { encryption = "on" @@ -172,10 +225,17 @@ func handleHTTP(router *weave.Router, httpAddr string) { muxRouter := mux.NewRouter() + if allocator != nil { + allocator.HandleHTTP(muxRouter) + } + muxRouter.Methods("GET").Path("/status").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprintln(w, "weave router", version) fmt.Fprintln(w, "Encryption", encryption) fmt.Fprintln(w, router.Status()) + if allocator != nil { + fmt.Fprintln(w, allocator.String()) + } }) muxRouter.Methods("GET").Path("/status-json").HandlerFunc(func(w http.ResponseWriter, r *http.Request) { From f6ac069bc52d697c441663c1e72dd8d675b160dd Mon Sep 17 00:00:00 2001 From: Bryan Boreham Date: Thu, 14 May 2015 17:57:13 +0100 Subject: [PATCH 4/7] Remove some less-necessary test helper functions --- ipam/http_test.go | 2 +- ipam/space/space_test.go | 6 +++--- testing/util.go | 23 ----------------------- 3 files changed, 4 insertions(+), 27 deletions(-) diff --git a/ipam/http_test.go b/ipam/http_test.go index 710bb4fb..a42bf5ba 100644 --- a/ipam/http_test.go +++ b/ipam/http_test.go @@ -74,7 +74,7 @@ func TestHttp(t *testing.T) { // Ask the http server for another address and check it's different cidr2 := HTTPPost(t, allocURL(port, container2)) - wt.AssertNotEqualString(t, cidr2, testAddr1+netSuffix, "address") + wt.AssertFalse(t, cidr2 == testAddr1+netSuffix, "address") // Ask for the first container again and we should get the same address again cidr1a := HTTPPost(t, allocURL(port, containerID)) diff --git a/ipam/space/space_test.go b/ipam/space/space_test.go index fd13c42c..d49324b7 100644 --- a/ipam/space/space_test.go +++ b/ipam/space/space_test.go @@ -101,7 +101,7 @@ func TestSpaceAllocate(t *testing.T) { space1.assertInvariants() _, addr2 := space1.Allocate() - wt.AssertNotEqualString(t, addr2.String(), testAddr1, "address") + wt.AssertFalse(t, addr2.String() == testAddr1, "address") wt.AssertEquals(t, space1.NumFreeAddresses(), address.Offset(18)) wt.AssertEquals(t, space1.NumFreeAddressesInRange(ip(testAddr1), ip(testAddrx)), address.Offset(13)) wt.AssertEquals(t, space1.NumFreeAddressesInRange(ip(testAddr1), ip(testAddry)), address.Offset(18)) @@ -194,7 +194,7 @@ func TestDonateSimple(t *testing.T) { // Empty space set should split in two and give me the second half start, numGivenUp, ok := ps1.Donate() - wt.AssertBool(t, ok, true, "Donate result") + wt.AssertTrue(t, ok, "Donate result") wt.AssertEqualString(t, start.String(), "10.0.1.24", "Invalid start") wt.AssertEquals(t, numGivenUp, address.Offset(24)) wt.AssertEquals(t, ps1.NumFreeAddresses(), address.Offset(24)) @@ -236,7 +236,7 @@ func TestDonateHard(t *testing.T) { // Now split newRange, numGivenUp, ok := spaceset.Donate() - wt.AssertBool(t, ok, true, "GiveUpSpace result") + wt.AssertTrue(t, ok, "GiveUpSpace result") wt.AssertEquals(t, newRange, ip("10.0.1.23")) wt.AssertEquals(t, numGivenUp, address.Offset(24)) wt.AssertEquals(t, spaceset.NumFreeAddresses(), address.Offset(23)) diff --git a/testing/util.go b/testing/util.go index c637eff5..1e45685a 100644 --- a/testing/util.go +++ b/testing/util.go @@ -40,29 +40,12 @@ func AssertNoErr(t *testing.T, err error) { } } -func AssertBool(t *testing.T, got, wanted bool, desc string) { - if got != wanted { - Fatalf(t, "Expected %s %t but got %t", desc, wanted, got) - } -} - -func AssertEqualUint32(t *testing.T, got, wanted uint32, desc string) { - if got != wanted { - Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) - } -} - func AssertEqualuint64(t *testing.T, got, wanted uint64, desc string) { if got != wanted { Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) } } -func AssertEqualInt64(t *testing.T, got, wanted int64, desc string) { - if got != wanted { - Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) - } -} func AssertEqualInt(t *testing.T, got, wanted int, desc string) { if got != wanted { Fatalf(t, "Expected %s %d but got %d", desc, wanted, got) @@ -81,12 +64,6 @@ func AssertEqualString(t *testing.T, got, wanted string, desc string) { } } -func AssertNotEqualString(t *testing.T, got, wanted string, desc string) { - if got == wanted { - Fatalf(t, "Expected %s unlike '%s'", desc, wanted) - } -} - func AssertEquals(t *testing.T, a1, a2 interface{}) { if !reflect.DeepEqual(a1, a2) { Fatalf(t, "Expected %+v == %+v", a1, a2) From a027337be2b010da7a6461747926edcc39db7d78 Mon Sep 17 00:00:00 2001 From: David Wragg Date: Fri, 15 May 2015 11:44:50 +0100 Subject: [PATCH 5/7] Tidy up IPAM setup --- weaver/main.go | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/weaver/main.go b/weaver/main.go index 85e5e8b8..5ef63a22 100644 --- a/weaver/main.go +++ b/weaver/main.go @@ -129,23 +129,26 @@ func main() { router := weave.NewRouter(config, name, nickName) log.Println("Our name is", router.Ourself) + var allocator *ipam.Allocator - if httpAddr != "" { - if iprangeCIDR != "" { - allocator = createAllocator(router, apiPath, iprangeCIDR, determineQuorum(peerCount, peers)) - } else { - if peerCount > 0 { - log.Fatal("-initpeercount flag specified without -iprange") - } - } - } - if httpAddr == "" || iprangeCIDR == "" { + if iprangeCIDR != "" { + allocator = createAllocator(router, apiPath, iprangeCIDR, determineQuorum(peerCount, peers)) + } else if peerCount > 0 { + log.Fatal("-initpeercount flag specified without -iprange") + } else { router.NewGossip("IPallocation", &ipam.DummyAllocator{}) } router.Start() initiateConnections(router, peers) - go handleHTTP(router, httpAddr, allocator) + + // The weave script always waits for a status call to succeed, + // so there is no point in doing "weave launch -httpaddr ''". + // This is here to support stand-alone use of weaver. + if httpAddr != "" { + go handleHTTP(router, httpAddr, allocator) + } + SignalHandlerLoop(router) } From 0eee999d7c58718827e0ba34d8cc37c09f568a6b Mon Sep 17 00:00:00 2001 From: David Wragg Date: Fri, 15 May 2015 11:49:36 +0100 Subject: [PATCH 6/7] Unify common call to wait_for_status upon "weave launch" --- weave | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/weave b/weave index 055ef756..10c3a766 100755 --- a/weave +++ b/weave @@ -646,12 +646,6 @@ populate_1_ipam() { done } -# Tell the newly-started weave IP allocator about existing weave IPs -populate_ipam() { - wait_for_status $CONTAINER_NAME $HTTP_PORT - with_container_addresses populate_1_ipam weave:expose $(docker ps -q) -} - # If we don't already have any CIDRs passed from the command-line, get one from IPAM # $1 is the name of the container and $2, if set, is the ID to use inside IPAM get_ipam_cidr() { @@ -783,7 +777,13 @@ case "$COMMAND" in -p $PORT:$CONTAINER_PORT/tcp -p $PORT:$CONTAINER_PORT/udp -e WEAVE_PASSWORD -v /var/run/docker.sock:/var/run/docker.sock \ $WEAVE_DOCKER_ARGS $IMAGE -iface $CONTAINER_IFNAME -port $CONTAINER_PORT -name "$PEERNAME" -nickname "$(hostname)" $IPRANGE "$@") with_container_netns $CONTAINER launch >/dev/null - [ -z "$IPRANGE" ] && wait_for_status $CONTAINER_NAME $HTTP_PORT || populate_ipam + + wait_for_status $CONTAINER_NAME $HTTP_PORT + if [ -n "$IPRANGE" ] ; then + # Tell the newly-started weave IP allocator about existing weave IPs + with_container_addresses populate_1_ipam weave:expose $(docker ps -q) + fi + echo $CONTAINER ;; launch-dns) From 4b942e8ed558e4dfe378057b25a03e5a8d08c0a6 Mon Sep 17 00:00:00 2001 From: David Wragg Date: Fri, 15 May 2015 13:12:36 +0100 Subject: [PATCH 7/7] populate_1_ipam -> ipam_claim --- weave | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/weave b/weave index 10c3a766..06a3dfc7 100755 --- a/weave +++ b/weave @@ -639,7 +639,9 @@ populate_dns() { # IP Allocation Management helpers ###################################################################### -populate_1_ipam() { +# Claim addresses for a container in IPAM. Expects to be called from +# with_container_addresses. +ipam_claim() { for CIDR in $3; do # NB: CONTAINER_IP is the IP of the weave container; it is set by wait_for_status. http_call_ip $CONTAINER_IP $HTTP_PORT PUT /ip/$1/${CIDR%/*} @@ -781,7 +783,7 @@ case "$COMMAND" in wait_for_status $CONTAINER_NAME $HTTP_PORT if [ -n "$IPRANGE" ] ; then # Tell the newly-started weave IP allocator about existing weave IPs - with_container_addresses populate_1_ipam weave:expose $(docker ps -q) + with_container_addresses ipam_claim weave:expose $(docker ps -q) fi echo $CONTAINER