|
@@ -0,0 +1,516 @@
|
|
|
+// Copyright 2015 flannel authors
|
|
|
+//
|
|
|
+// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
+// you may not use this file except in compliance with the License.
|
|
|
+// You may obtain a copy of the License at
|
|
|
+//
|
|
|
+// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
+//
|
|
|
+// Unless required by applicable law or agreed to in writing, software
|
|
|
+// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
+// See the License for the specific language governing permissions and
|
|
|
+// limitations under the License.
|
|
|
+
|
|
|
+package subnet
|
|
|
+
|
|
|
+import (
|
|
|
+ "errors"
|
|
|
+ "fmt"
|
|
|
+ "strconv"
|
|
|
+ "time"
|
|
|
+
|
|
|
+ etcd "github.com/coreos/etcd/client"
|
|
|
+ "github.com/coreos/flannel/pkg/ip"
|
|
|
+ log "github.com/golang/glog"
|
|
|
+ "golang.org/x/net/context"
|
|
|
+)
|
|
|
+
|
|
|
+const (
|
|
|
+ raceRetries = 10
|
|
|
+ subnetTTL = 24 * time.Hour
|
|
|
+)
|
|
|
+
|
|
|
+type LocalManager struct {
|
|
|
+ registry Registry
|
|
|
+}
|
|
|
+
|
|
|
+type watchCursor struct {
|
|
|
+ index uint64
|
|
|
+}
|
|
|
+
|
|
|
+func isErrEtcdTestFailed(e error) bool {
|
|
|
+ if e == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ etcdErr, ok := e.(etcd.Error)
|
|
|
+ return ok && etcdErr.Code == etcd.ErrorCodeTestFailed
|
|
|
+}
|
|
|
+
|
|
|
+func isErrEtcdNodeExist(e error) bool {
|
|
|
+ if e == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ etcdErr, ok := e.(etcd.Error)
|
|
|
+ return ok || etcdErr.Code == etcd.ErrorCodeNodeExist
|
|
|
+}
|
|
|
+
|
|
|
+func isErrEtcdKeyNotFound(e error) bool {
|
|
|
+ if e == nil {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ etcdErr, ok := e.(etcd.Error)
|
|
|
+ return ok || etcdErr.Code == etcd.ErrorCodeKeyNotFound
|
|
|
+}
|
|
|
+
|
|
|
+func (c watchCursor) String() string {
|
|
|
+ return strconv.FormatUint(c.index, 10)
|
|
|
+}
|
|
|
+
|
|
|
+func NewLocalManager(config *EtcdConfig) (Manager, error) {
|
|
|
+ r, err := newEtcdSubnetRegistry(config, nil)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ return newLocalManager(r), nil
|
|
|
+}
|
|
|
+
|
|
|
+func newLocalManager(r Registry) Manager {
|
|
|
+ return &LocalManager{
|
|
|
+ registry: r,
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) GetNetworkConfig(ctx context.Context, network string) (*Config, error) {
|
|
|
+ cfg, err := m.registry.getNetworkConfig(ctx, network)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return ParseConfig(cfg)
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) AcquireLease(ctx context.Context, network string, attrs *LeaseAttrs) (*Lease, error) {
|
|
|
+ config, err := m.GetNetworkConfig(ctx, network)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ for i := 0; i < raceRetries; i++ {
|
|
|
+ l, err := m.tryAcquireLease(ctx, network, config, attrs.PublicIP, attrs)
|
|
|
+ switch err {
|
|
|
+ case nil:
|
|
|
+ return l, nil
|
|
|
+ case errTryAgain:
|
|
|
+ continue
|
|
|
+ default:
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil, errors.New("Max retries reached trying to acquire a subnet")
|
|
|
+}
|
|
|
+
|
|
|
+func findLeaseByIP(leases []Lease, pubIP ip.IP4) *Lease {
|
|
|
+ for _, l := range leases {
|
|
|
+ if pubIP == l.Attrs.PublicIP {
|
|
|
+ return &l
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) tryAcquireLease(ctx context.Context, network string, config *Config, extIaddr ip.IP4, attrs *LeaseAttrs) (*Lease, error) {
|
|
|
+ leases, _, err := m.registry.getSubnets(ctx, network)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ // try to reuse a subnet if there's one that matches our IP
|
|
|
+ if l := findLeaseByIP(leases, extIaddr); l != nil {
|
|
|
+ // make sure the existing subnet is still within the configured network
|
|
|
+ if isSubnetConfigCompat(config, l.Subnet) {
|
|
|
+ log.Infof("Found lease (%v) for current IP (%v), reusing", l.Subnet, extIaddr)
|
|
|
+
|
|
|
+ ttl := time.Duration(0)
|
|
|
+ if !l.Expiration.IsZero() {
|
|
|
+ // Not a reservation
|
|
|
+ ttl = subnetTTL
|
|
|
+ }
|
|
|
+ exp, err := m.registry.updateSubnet(ctx, network, l.Subnet, attrs, ttl, 0)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ l.Attrs = *attrs
|
|
|
+ l.Expiration = exp
|
|
|
+ return l, nil
|
|
|
+ } else {
|
|
|
+ log.Infof("Found lease (%v) for current IP (%v) but not compatible with current config, deleting", l.Subnet, extIaddr)
|
|
|
+ if err := m.registry.deleteSubnet(ctx, network, l.Subnet); err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // no existing match, grab a new one
|
|
|
+ sn, err := m.allocateSubnet(config, leases)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ exp, err := m.registry.createSubnet(ctx, network, sn, attrs, subnetTTL)
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return &Lease{
|
|
|
+ Subnet: sn,
|
|
|
+ Attrs: *attrs,
|
|
|
+ Expiration: exp,
|
|
|
+ }, nil
|
|
|
+ case isErrEtcdNodeExist(err):
|
|
|
+ return nil, errTryAgain
|
|
|
+ default:
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) allocateSubnet(config *Config, leases []Lease) (ip.IP4Net, error) {
|
|
|
+ log.Infof("Picking subnet in range %s ... %s", config.SubnetMin, config.SubnetMax)
|
|
|
+
|
|
|
+ var bag []ip.IP4
|
|
|
+ sn := ip.IP4Net{IP: config.SubnetMin, PrefixLen: config.SubnetLen}
|
|
|
+
|
|
|
+OuterLoop:
|
|
|
+ for ; sn.IP <= config.SubnetMax && len(bag) < 100; sn = sn.Next() {
|
|
|
+ for _, l := range leases {
|
|
|
+ if sn.Overlaps(l.Subnet) {
|
|
|
+ continue OuterLoop
|
|
|
+ }
|
|
|
+ }
|
|
|
+ bag = append(bag, sn.IP)
|
|
|
+ }
|
|
|
+
|
|
|
+ if len(bag) == 0 {
|
|
|
+ return ip.IP4Net{}, errors.New("out of subnets")
|
|
|
+ } else {
|
|
|
+ i := randInt(0, len(bag))
|
|
|
+ return ip.IP4Net{IP: bag[i], PrefixLen: config.SubnetLen}, nil
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) RevokeLease(ctx context.Context, network string, sn ip.IP4Net) error {
|
|
|
+ return m.registry.deleteSubnet(ctx, network, sn)
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) RenewLease(ctx context.Context, network string, lease *Lease) error {
|
|
|
+ exp, err := m.registry.updateSubnet(ctx, network, lease.Subnet, &lease.Attrs, subnetTTL, 0)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ lease.Expiration = exp
|
|
|
+ return nil
|
|
|
+}
|
|
|
+
|
|
|
+func getNextIndex(cursor interface{}) (uint64, error) {
|
|
|
+ nextIndex := uint64(0)
|
|
|
+
|
|
|
+ if wc, ok := cursor.(watchCursor); ok {
|
|
|
+ nextIndex = wc.index
|
|
|
+ } else if s, ok := cursor.(string); ok {
|
|
|
+ var err error
|
|
|
+ nextIndex, err = strconv.ParseUint(s, 10, 64)
|
|
|
+ if err != nil {
|
|
|
+ return 0, fmt.Errorf("failed to parse cursor: %v", err)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return 0, fmt.Errorf("internal error: watch cursor is of unknown type")
|
|
|
+ }
|
|
|
+
|
|
|
+ return nextIndex, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) leaseWatchReset(ctx context.Context, network string, sn ip.IP4Net) (LeaseWatchResult, error) {
|
|
|
+ l, index, err := m.registry.getSubnet(ctx, network, sn)
|
|
|
+ if err != nil {
|
|
|
+ return LeaseWatchResult{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ return LeaseWatchResult{
|
|
|
+ Snapshot: []Lease{*l},
|
|
|
+ Cursor: watchCursor{index},
|
|
|
+ }, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) WatchLease(ctx context.Context, network string, sn ip.IP4Net, cursor interface{}) (LeaseWatchResult, error) {
|
|
|
+ if cursor == nil {
|
|
|
+ return m.leaseWatchReset(ctx, network, sn)
|
|
|
+ }
|
|
|
+
|
|
|
+ nextIndex, err := getNextIndex(cursor)
|
|
|
+ if err != nil {
|
|
|
+ return LeaseWatchResult{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ evt, index, err := m.registry.watchSubnet(ctx, network, nextIndex, sn)
|
|
|
+
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return LeaseWatchResult{
|
|
|
+ Events: []Event{evt},
|
|
|
+ Cursor: watchCursor{index},
|
|
|
+ }, nil
|
|
|
+
|
|
|
+ case isIndexTooSmall(err):
|
|
|
+ log.Warning("Watch of subnet leases failed because etcd index outside history window")
|
|
|
+ return m.leaseWatchReset(ctx, network, sn)
|
|
|
+
|
|
|
+ default:
|
|
|
+ return LeaseWatchResult{}, err
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) WatchLeases(ctx context.Context, network string, cursor interface{}) (LeaseWatchResult, error) {
|
|
|
+ if cursor == nil {
|
|
|
+ return m.leasesWatchReset(ctx, network)
|
|
|
+ }
|
|
|
+
|
|
|
+ nextIndex, err := getNextIndex(cursor)
|
|
|
+ if err != nil {
|
|
|
+ return LeaseWatchResult{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ evt, index, err := m.registry.watchSubnets(ctx, network, nextIndex)
|
|
|
+
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return LeaseWatchResult{
|
|
|
+ Events: []Event{evt},
|
|
|
+ Cursor: watchCursor{index},
|
|
|
+ }, nil
|
|
|
+
|
|
|
+ case isIndexTooSmall(err):
|
|
|
+ log.Warning("Watch of subnet leases failed because etcd index outside history window")
|
|
|
+ return m.leasesWatchReset(ctx, network)
|
|
|
+
|
|
|
+ default:
|
|
|
+ return LeaseWatchResult{}, err
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) WatchNetworks(ctx context.Context, cursor interface{}) (NetworkWatchResult, error) {
|
|
|
+ if cursor == nil {
|
|
|
+ return m.networkWatchReset(ctx)
|
|
|
+ }
|
|
|
+
|
|
|
+ nextIndex, err := getNextIndex(cursor)
|
|
|
+ if err != nil {
|
|
|
+ return NetworkWatchResult{}, err
|
|
|
+ }
|
|
|
+
|
|
|
+ for {
|
|
|
+ evt, index, err := m.registry.watchNetworks(ctx, nextIndex)
|
|
|
+
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return NetworkWatchResult{
|
|
|
+ Events: []Event{evt},
|
|
|
+ Cursor: watchCursor{index},
|
|
|
+ }, nil
|
|
|
+
|
|
|
+ case err == errTryAgain:
|
|
|
+ nextIndex = index
|
|
|
+
|
|
|
+ case isIndexTooSmall(err):
|
|
|
+ log.Warning("Watch of networks failed because etcd index outside history window")
|
|
|
+ return m.networkWatchReset(ctx)
|
|
|
+
|
|
|
+ default:
|
|
|
+ return NetworkWatchResult{}, err
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+func isIndexTooSmall(err error) bool {
|
|
|
+ etcdErr, ok := err.(etcd.Error)
|
|
|
+ return ok && etcdErr.Code == etcd.ErrorCodeEventIndexCleared
|
|
|
+}
|
|
|
+
|
|
|
+// leasesWatchReset is called when incremental lease watch failed and we need to grab a snapshot
|
|
|
+func (m *LocalManager) leasesWatchReset(ctx context.Context, network string) (LeaseWatchResult, error) {
|
|
|
+ wr := LeaseWatchResult{}
|
|
|
+
|
|
|
+ leases, index, err := m.registry.getSubnets(ctx, network)
|
|
|
+ if err != nil {
|
|
|
+ return wr, fmt.Errorf("failed to retrieve subnet leases: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ wr.Cursor = watchCursor{index}
|
|
|
+ wr.Snapshot = leases
|
|
|
+ return wr, nil
|
|
|
+}
|
|
|
+
|
|
|
+// networkWatchReset is called when incremental network watch failed and we need to grab a snapshot
|
|
|
+func (m *LocalManager) networkWatchReset(ctx context.Context) (NetworkWatchResult, error) {
|
|
|
+ wr := NetworkWatchResult{}
|
|
|
+
|
|
|
+ networks, index, err := m.registry.getNetworks(ctx)
|
|
|
+ if err != nil {
|
|
|
+ return wr, fmt.Errorf("failed to retrieve networks: %v", err)
|
|
|
+ }
|
|
|
+
|
|
|
+ wr.Cursor = watchCursor{index}
|
|
|
+ wr.Snapshot = networks
|
|
|
+ return wr, nil
|
|
|
+}
|
|
|
+
|
|
|
+func isSubnetConfigCompat(config *Config, sn ip.IP4Net) bool {
|
|
|
+ if sn.IP < config.SubnetMin || sn.IP > config.SubnetMax {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+
|
|
|
+ return sn.PrefixLen == config.SubnetLen
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) tryAddReservation(ctx context.Context, network string, r *Reservation) error {
|
|
|
+ attrs := &LeaseAttrs{
|
|
|
+ PublicIP: r.PublicIP,
|
|
|
+ }
|
|
|
+
|
|
|
+ _, err := m.registry.createSubnet(ctx, network, r.Subnet, attrs, 0)
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return nil
|
|
|
+
|
|
|
+ case !isErrEtcdNodeExist(err):
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // This subnet or its reservation already exists.
|
|
|
+ // Get what's there and
|
|
|
+ // - if PublicIP matches, remove the TTL make it a reservation
|
|
|
+ // - otherwise, error out
|
|
|
+ sub, asof, err := m.registry.getSubnet(ctx, network, r.Subnet)
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ case isErrEtcdKeyNotFound(err):
|
|
|
+ // Subnet just got expired or was deleted
|
|
|
+ return errTryAgain
|
|
|
+ default:
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if sub.Attrs.PublicIP != r.PublicIP {
|
|
|
+ // Subnet already taken
|
|
|
+ return ErrLeaseTaken
|
|
|
+ }
|
|
|
+
|
|
|
+ // remove TTL
|
|
|
+ _, err = m.registry.updateSubnet(ctx, network, r.Subnet, &sub.Attrs, 0, asof)
|
|
|
+ if isErrEtcdTestFailed(err) {
|
|
|
+ return errTryAgain
|
|
|
+ }
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) AddReservation(ctx context.Context, network string, r *Reservation) error {
|
|
|
+ config, err := m.GetNetworkConfig(ctx, network)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ if config.SubnetLen != r.Subnet.PrefixLen {
|
|
|
+ return fmt.Errorf("reservation subnet has mask incompatible with network config")
|
|
|
+ }
|
|
|
+
|
|
|
+ if !config.Network.Overlaps(r.Subnet) {
|
|
|
+ return fmt.Errorf("reservation subnet is outside of flannel network")
|
|
|
+ }
|
|
|
+
|
|
|
+ for i := 0; i < raceRetries; i++ {
|
|
|
+ err := m.tryAddReservation(ctx, network, r)
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return nil
|
|
|
+ case err == errTryAgain:
|
|
|
+ continue
|
|
|
+ default:
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ErrNoMoreTries
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) tryRemoveReservation(ctx context.Context, network string, subnet ip.IP4Net) error {
|
|
|
+ sub, asof, err := m.registry.getSubnet(ctx, network, subnet)
|
|
|
+ if err != nil {
|
|
|
+ return err
|
|
|
+ }
|
|
|
+
|
|
|
+ // add back the TTL
|
|
|
+ _, err = m.registry.updateSubnet(ctx, network, subnet, &sub.Attrs, subnetTTL, asof)
|
|
|
+ if isErrEtcdTestFailed(err) {
|
|
|
+ return errTryAgain
|
|
|
+ }
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+//RemoveReservation removes the subnet by setting TTL back to subnetTTL (24hours)
|
|
|
+func (m *LocalManager) RemoveReservation(ctx context.Context, network string, subnet ip.IP4Net) error {
|
|
|
+ for i := 0; i < raceRetries; i++ {
|
|
|
+ err := m.tryRemoveReservation(ctx, network, subnet)
|
|
|
+ switch {
|
|
|
+ case err == nil:
|
|
|
+ return nil
|
|
|
+ case err == errTryAgain:
|
|
|
+ continue
|
|
|
+ default:
|
|
|
+ return err
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return ErrNoMoreTries
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) ListReservations(ctx context.Context, network string) ([]Reservation, error) {
|
|
|
+ subnets, _, err := m.registry.getSubnets(ctx, network)
|
|
|
+ if err != nil {
|
|
|
+ return nil, err
|
|
|
+ }
|
|
|
+
|
|
|
+ rsvs := []Reservation{}
|
|
|
+ for _, sub := range subnets {
|
|
|
+ // Reservations don't have TTL and so no expiration
|
|
|
+ if !sub.Expiration.IsZero() {
|
|
|
+ continue
|
|
|
+ }
|
|
|
+
|
|
|
+ r := Reservation{
|
|
|
+ Subnet: sub.Subnet,
|
|
|
+ PublicIP: sub.Attrs.PublicIP,
|
|
|
+ }
|
|
|
+ rsvs = append(rsvs, r)
|
|
|
+ }
|
|
|
+
|
|
|
+ return rsvs, nil
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) CreateBackendData(ctx context.Context, network, data string) error {
|
|
|
+ err := m.registry.createBackendData(ctx, network, data)
|
|
|
+
|
|
|
+ if err == nil {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ if etcdErr, ok := err.(etcd.Error); ok && etcdErr.Code == etcd.ErrorCodeNodeExist {
|
|
|
+ return nil
|
|
|
+ }
|
|
|
+
|
|
|
+ return err
|
|
|
+}
|
|
|
+
|
|
|
+func (m *LocalManager) GetBackendData(ctx context.Context, network string) (string, error) {
|
|
|
+ return m.registry.getBackendData(ctx, network)
|
|
|
+}
|