From 87b54ff7f3ff93da02368191e5e8d5ff7e406d1e Mon Sep 17 00:00:00 2001 From: VZ Cambria Date: Tue, 27 Dec 2016 14:35:12 -0500 Subject: [PATCH] Parsing changes for labels and extra CNI_ARGS --- mcc-manual-add-anycast.sh | 21 ++ mcc-manual-add-static-env.sh | 24 ++ mcc-manual-add-static.sh | 21 ++ mcc-manual-del-anycast.sh | 21 ++ mcc-manual-del-static-env.sh | 24 ++ mcc-manual-del-static.sh | 21 ++ plugins/ipam/vz-local/#allocator.go# | 272 ++++++++++++++ plugins/ipam/vz-local/README.md | 86 +++++ plugins/ipam/vz-local/allocator.go | 273 ++++++++++++++ plugins/ipam/vz-local/allocator_test.go | 355 ++++++++++++++++++ plugins/ipam/vz-local/backend/disk/backend.go | 110 ++++++ plugins/ipam/vz-local/backend/disk/lock.go | 50 +++ plugins/ipam/vz-local/backend/store.go | 27 ++ .../vz-local/backend/testing/fake_store.go | 72 ++++ plugins/ipam/vz-local/config.go | 193 ++++++++++ .../ipam/vz-local/custom/hack-types-dot-go | 32 ++ .../ipam/vz-local/host_local_suite_test.go | 27 ++ plugins/ipam/vz-local/host_local_test.go | 92 +++++ plugins/ipam/vz-local/main.go | 75 ++++ 19 files changed, 1796 insertions(+) create mode 100755 mcc-manual-add-anycast.sh create mode 100755 mcc-manual-add-static-env.sh create mode 100755 mcc-manual-add-static.sh create mode 100755 mcc-manual-del-anycast.sh create mode 100755 mcc-manual-del-static-env.sh create mode 100755 mcc-manual-del-static.sh create mode 100644 plugins/ipam/vz-local/#allocator.go# create mode 100644 plugins/ipam/vz-local/README.md create mode 100644 plugins/ipam/vz-local/allocator.go create mode 100644 plugins/ipam/vz-local/allocator_test.go create mode 100644 plugins/ipam/vz-local/backend/disk/backend.go create mode 100644 plugins/ipam/vz-local/backend/disk/lock.go create mode 100644 plugins/ipam/vz-local/backend/store.go create mode 100644 plugins/ipam/vz-local/backend/testing/fake_store.go create mode 100644 plugins/ipam/vz-local/config.go create mode 100644 plugins/ipam/vz-local/custom/hack-types-dot-go create mode 100644 plugins/ipam/vz-local/host_local_suite_test.go create mode 100644 plugins/ipam/vz-local/host_local_test.go create mode 100644 plugins/ipam/vz-local/main.go diff --git a/mcc-manual-add-anycast.sh b/mcc-manual-add-anycast.sh new file mode 100755 index 0000000..a0ac19a --- /dev/null +++ b/mcc-manual-add-anycast.sh @@ -0,0 +1,21 @@ +#/bin/bash +export DEBUG=1 +export NETCONFPATH=/etc/cni/net.d/ +export CNI_PATH=/home/mcambria/go/src/github.com/containernetworking/cni/bin/ +export NETCONFPATH=${NETCONFPATH-/etc/cni/net.d} +export CNI_COMMAND=ADD +export CNI_NETNS=/var/run/netns/mcc-cni-test0 +export CNI_CONTAINERID=mcc-cni-test0 + +# export CNI_ARGS="IP=172.19.99.99" + +export PATH=$CNI_PATH:$PATH +export CNI_IFNAME=eth1 + +# sudo -E /home/mcambria/github/cni-master/scripts/exec-plugins.sh add mcc-cni-test0 /var/run/netns/mcc-cni-test0 + +# echo $CNI_ARGS + +macvlan masklen-2 { + return nil, fmt.Errorf("Network %v too small to allocate from", conf.Subnet) + } + + var ( + start net.IP + end net.IP + err error + ) + start, end, err = networkRange((*net.IPNet)(&conf.Subnet)) + if err != nil { + return nil, err + } + + // skip the .0 address + start = ip.NextIP(start) + + if conf.RangeStart != nil { + if err := validateRangeIP(conf.RangeStart, (*net.IPNet)(&conf.Subnet), nil, nil); err != nil { + return nil, err + } + start = conf.RangeStart + } + if conf.RangeEnd != nil { + if err := validateRangeIP(conf.RangeEnd, (*net.IPNet)(&conf.Subnet), start, nil); err != nil { + return nil, err + } + end = conf.RangeEnd + } + return &IPAllocator{start, end, conf, store}, nil +} + +func canonicalizeIP(ip net.IP) (net.IP, error) { + if ip.To4() != nil { + return ip.To4(), nil + } else if ip.To16() != nil { + return ip.To16(), nil + } + return nil, fmt.Errorf("IP %s not v4 nor v6", ip) +} + +// Ensures @ip is within @ipnet, and (if given) inclusive of @start and @end +func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) error { + var err error + + // Make sure we can compare IPv4 addresses directly + ip, err = canonicalizeIP(ip) + if err != nil { + return err + } + + if !ipnet.Contains(ip) { + return fmt.Errorf("%s not in network: %s", ip, ipnet) + } + + if start != nil { + start, err = canonicalizeIP(start) + if err != nil { + return err + } + if len(ip) != len(start) { + return fmt.Errorf("%s %d not same size IP address as start %s %d", ip, len(ip), start, len(start)) + } + for i := 0; i < len(ip); i++ { + if ip[i] > start[i] { + break + } else if ip[i] < start[i] { + return fmt.Errorf("%s outside of network %s with start %s", ip, ipnet, start) + } + } + } + + if end != nil { + end, err = canonicalizeIP(end) + if err != nil { + return err + } + if len(ip) != len(end) { + return fmt.Errorf("%s %d not same size IP address as end %s %d", ip, len(ip), end, len(end)) + } + for i := 0; i < len(ip); i++ { + if ip[i] < end[i] { + break + } else if ip[i] > end[i] { + return fmt.Errorf("%s outside of network %s with end %s", ip, ipnet, end) + } + } + } + return nil +} + +// Returns newly allocated IP along with its config +func (a *IPAllocator) Get(id string) (*types.IPConfig, error) { + a.store.Lock() + defer a.store.Unlock() + + gw := a.conf.Gateway + if gw == nil { + gw = ip.NextIP(a.conf.Subnet.IP) + } + + var requestedIP net.IP + if a.conf.Args != nil { + requestedIP = a.conf.Args.IP + } + + if requestedIP != nil { + + if gw != nil && gw.Equal(a.conf.Args.IP) { + //log.Println("AdvRoute: BGP Advertises /32", requestedIP, advertise_route) + } + + subnet := net.IPNet{ + IP: a.conf.Subnet.IP, + Mask: a.conf.Subnet.Mask, + } + err := validateRangeIP(requestedIP, &subnet, a.start, a.end) + if err != nil { + return nil, err + } + + reserved, err := a.store.Reserve(id, requestedIP) + if err != nil { + return nil, err + } + + if reserved { + return &types.IPConfig{ + IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask}, + Gateway: gw, + Routes: a.conf.Routes, + }, nil + } + return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name) + } + + startIP, endIP := a.getSearchRange() + for cur := startIP; ; cur = a.nextIP(cur) { + // don't allocate gateway IP + if gw != nil && cur.Equal(gw) { + continue + } + + reserved, err := a.store.Reserve(id, cur) + if err != nil { + return nil, err + } + if reserved { + return &types.IPConfig{ + IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask}, + Gateway: gw, + Routes: a.conf.Routes, + }, nil + } + // break here to complete the loop + if cur.Equal(endIP) { + break + } + } + return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name) +} + +// Releases all IPs allocated for the container with given ID +func (a *IPAllocator) Release(id string) error { + a.store.Lock() + defer a.store.Unlock() + + return a.store.ReleaseByID(id) +} + +// Return the start and end IP addresses of a given subnet, excluding +// the broadcast address (eg, 192.168.1.255) +func networkRange(ipnet *net.IPNet) (net.IP, net.IP, error) { + if ipnet.IP == nil { + return nil, nil, fmt.Errorf("missing field %q in IPAM configuration", "subnet") + } + ip, err := canonicalizeIP(ipnet.IP) + if err != nil { + return nil, nil, fmt.Errorf("IP not v4 nor v6") + } + + if len(ip) != len(ipnet.Mask) { + return nil, nil, fmt.Errorf("IPNet IP and Mask version mismatch") + } + + var end net.IP + for i := 0; i < len(ip); i++ { + end = append(end, ip[i]|^ipnet.Mask[i]) + } + + // Exclude the broadcast address for IPv4 + if ip.To4() != nil { + end[3]-- + } + + return ipnet.IP, end, nil +} + +// nextIP returns the next ip of curIP within ipallocator's subnet +func (a *IPAllocator) nextIP(curIP net.IP) net.IP { + if curIP.Equal(a.end) { + return a.start + } + return ip.NextIP(curIP) +} + +// getSearchRange returns the start and end ip based on the last reserved ip +func (a *IPAllocator) getSearchRange() (net.IP, net.IP) { + var startIP net.IP + var endIP net.IP + startFromLastReservedIP := false + lastReservedIP, err := a.store.LastReservedIP() + if err != nil { + log.Printf("Error retriving last reserved ip: %v", err) + } else if lastReservedIP != nil { + subnet := net.IPNet{ + IP: a.conf.Subnet.IP, + Mask: a.conf.Subnet.Mask, + } + err := validateRangeIP(lastReservedIP, &subnet, a.start, a.end) + if err == nil { + startFromLastReservedIP = true + } + } + if startFromLastReservedIP { + startIP = a.nextIP(lastReservedIP) + endIP = lastReservedIP + } else { + startIP = a.start + endIP = a.end + } + return startIP, endIP +} diff --git a/plugins/ipam/vz-local/README.md b/plugins/ipam/vz-local/README.md new file mode 100644 index 0000000..39e9ede --- /dev/null +++ b/plugins/ipam/vz-local/README.md @@ -0,0 +1,86 @@ +# host-local IP address manager + +host-local IPAM allocates IPv4 and IPv6 addresses out of a specified address range. + +## Usage + +### Obtain an IP + +Given the following network configuration: + +``` +{ + "name": "default", + "ipam": { + "type": "host-local", + "subnet": "203.0.113.0/24" + } +} +``` + +#### Using the command line interface + +``` +$ export CNI_COMMAND=ADD +$ export CNI_CONTAINERID=f81d4fae-7dec-11d0-a765-00a0c91e6bf6 +$ ./host-local < $conf +``` + +``` +{ + "ip4": { + "ip": "203.0.113.1/24" + } +} +``` + +## Backends + +By default ipmanager stores IP allocations on the local filesystem using the IP address as the file name and the ID as contents. For example: + +``` +$ ls /var/lib/cni/networks/default +``` +``` +203.0.113.1 203.0.113.2 +``` + +``` +$ cat /var/lib/cni/networks/default/203.0.113.1 +``` +``` +f81d4fae-7dec-11d0-a765-00a0c91e6bf6 +``` + +## Configuration Files + + +``` +{ + "name": "ipv6", + "ipam": { + "type": "host-local", + "subnet": "3ffe:ffff:0:01ff::/64", + "range-start": "3ffe:ffff:0:01ff::0010", + "range-end": "3ffe:ffff:0:01ff::0020", + "routes": [ + { "dst": "3ffe:ffff:0:01ff::1/64" } + ] + } +} +``` + +``` +{ + "name": "ipv4", + "ipam": { + "type": "host-local", + "subnet": "203.0.113.1/24", + "range-start": "203.0.113.10", + "range-end": "203.0.113.20", + "routes": [ + { "dst": "203.0.113.0/24" } + ] + } +} +``` diff --git a/plugins/ipam/vz-local/allocator.go b/plugins/ipam/vz-local/allocator.go new file mode 100644 index 0000000..b8c0ad0 --- /dev/null +++ b/plugins/ipam/vz-local/allocator.go @@ -0,0 +1,273 @@ +// Copyright 2015 CNI 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 main + +import ( + "fmt" + "log" + "net" + + "github.com/containernetworking/cni/pkg/ip" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/plugins/ipam/host-local/backend" +) + +type IPAllocator struct { + // start is inclusive and may be allocated + start net.IP + // end is inclusive and may be allocated + end net.IP + conf *IPAMConfig + store backend.Store +} + +func NewIPAllocator(conf *IPAMConfig, store backend.Store) (*IPAllocator, error) { + // Can't create an allocator for a network with no addresses, eg + // a /32 or /31 + ones, masklen := conf.Subnet.Mask.Size() + if ones > masklen-2 { + return nil, fmt.Errorf("Network %v too small to allocate from", conf.Subnet) + } + + var ( + start net.IP + end net.IP + err error + ) + start, end, err = networkRange((*net.IPNet)(&conf.Subnet)) + if err != nil { + return nil, err + } + + // skip the .0 address + start = ip.NextIP(start) + + if conf.RangeStart != nil { + if err := validateRangeIP(conf.RangeStart, (*net.IPNet)(&conf.Subnet), nil, nil); err != nil { + return nil, err + } + start = conf.RangeStart + } + if conf.RangeEnd != nil { + if err := validateRangeIP(conf.RangeEnd, (*net.IPNet)(&conf.Subnet), start, nil); err != nil { + return nil, err + } + end = conf.RangeEnd + } + return &IPAllocator{start, end, conf, store}, nil +} + +func canonicalizeIP(ip net.IP) (net.IP, error) { + if ip.To4() != nil { + return ip.To4(), nil + } else if ip.To16() != nil { + return ip.To16(), nil + } + return nil, fmt.Errorf("IP %s not v4 nor v6", ip) +} + +// Ensures @ip is within @ipnet, and (if given) inclusive of @start and @end +func validateRangeIP(ip net.IP, ipnet *net.IPNet, start net.IP, end net.IP) error { + var err error + + // Make sure we can compare IPv4 addresses directly + ip, err = canonicalizeIP(ip) + if err != nil { + return err + } + + if !ipnet.Contains(ip) { + return fmt.Errorf("%s not in network: %s", ip, ipnet) + } + + if start != nil { + start, err = canonicalizeIP(start) + if err != nil { + return err + } + if len(ip) != len(start) { + return fmt.Errorf("%s %d not same size IP address as start %s %d", ip, len(ip), start, len(start)) + } + for i := 0; i < len(ip); i++ { + if ip[i] > start[i] { + break + } else if ip[i] < start[i] { + return fmt.Errorf("%s outside of network %s with start %s", ip, ipnet, start) + } + } + } + + if end != nil { + end, err = canonicalizeIP(end) + if err != nil { + return err + } + if len(ip) != len(end) { + return fmt.Errorf("%s %d not same size IP address as end %s %d", ip, len(ip), end, len(end)) + } + for i := 0; i < len(ip); i++ { + if ip[i] < end[i] { + break + } else if ip[i] > end[i] { + return fmt.Errorf("%s outside of network %s with end %s", ip, ipnet, end) + } + } + } + return nil +} + +// Returns newly allocated IP along with its config +func (a *IPAllocator) Get(id string) (*types.IPConfig, error) { + a.store.Lock() + defer a.store.Unlock() + + gw := a.conf.Gateway + if gw == nil { + gw = ip.NextIP(a.conf.Subnet.IP) + } + + var requestedIP net.IP + if a.conf.Args != nil { + requestedIP = a.conf.Args.IP + } + + if requestedIP != nil { + if gw != nil && gw.Equal(a.conf.Args.IP) { + return nil, fmt.Errorf("requested IP must differ gateway IP") + } + + //log.Println("AdvRoute: BGP Advertises /32", requestedIP, advertise_route) + log.Println("AdvRoute: BGP Advertises /32", requestedIP) + subnet := net.IPNet{ + IP: a.conf.Subnet.IP, + Mask: a.conf.Subnet.Mask, + } + err := validateRangeIP(requestedIP, &subnet, a.start, a.end) + if err != nil { + return nil, err + } + + reserved, err := a.store.Reserve(id, requestedIP) + if err != nil { + return nil, err + } + + if reserved { + return &types.IPConfig{ + IP: net.IPNet{IP: requestedIP, Mask: a.conf.Subnet.Mask}, + Gateway: gw, + Routes: a.conf.Routes, + }, nil + } + return nil, fmt.Errorf("requested IP address %q is not available in network: %s", requestedIP, a.conf.Name) + } + + startIP, endIP := a.getSearchRange() + for cur := startIP; ; cur = a.nextIP(cur) { + // don't allocate gateway IP + if gw != nil && cur.Equal(gw) { + continue + } + + reserved, err := a.store.Reserve(id, cur) + if err != nil { + return nil, err + } + if reserved { + return &types.IPConfig{ + IP: net.IPNet{IP: cur, Mask: a.conf.Subnet.Mask}, + Gateway: gw, + Routes: a.conf.Routes, + }, nil + } + // break here to complete the loop + if cur.Equal(endIP) { + break + } + } + return nil, fmt.Errorf("no IP addresses available in network: %s", a.conf.Name) +} + +// Releases all IPs allocated for the container with given ID +func (a *IPAllocator) Release(id string) error { + a.store.Lock() + defer a.store.Unlock() + + return a.store.ReleaseByID(id) +} + +// Return the start and end IP addresses of a given subnet, excluding +// the broadcast address (eg, 192.168.1.255) +func networkRange(ipnet *net.IPNet) (net.IP, net.IP, error) { + if ipnet.IP == nil { + return nil, nil, fmt.Errorf("missing field %q in IPAM configuration", "subnet") + } + ip, err := canonicalizeIP(ipnet.IP) + if err != nil { + return nil, nil, fmt.Errorf("IP not v4 nor v6") + } + + if len(ip) != len(ipnet.Mask) { + return nil, nil, fmt.Errorf("IPNet IP and Mask version mismatch") + } + + var end net.IP + for i := 0; i < len(ip); i++ { + end = append(end, ip[i]|^ipnet.Mask[i]) + } + + // Exclude the broadcast address for IPv4 + if ip.To4() != nil { + end[3]-- + } + + return ipnet.IP, end, nil +} + +// nextIP returns the next ip of curIP within ipallocator's subnet +func (a *IPAllocator) nextIP(curIP net.IP) net.IP { + if curIP.Equal(a.end) { + return a.start + } + return ip.NextIP(curIP) +} + +// getSearchRange returns the start and end ip based on the last reserved ip +func (a *IPAllocator) getSearchRange() (net.IP, net.IP) { + var startIP net.IP + var endIP net.IP + startFromLastReservedIP := false + lastReservedIP, err := a.store.LastReservedIP() + if err != nil { + log.Printf("Error retriving last reserved ip: %v", err) + } else if lastReservedIP != nil { + subnet := net.IPNet{ + IP: a.conf.Subnet.IP, + Mask: a.conf.Subnet.Mask, + } + err := validateRangeIP(lastReservedIP, &subnet, a.start, a.end) + if err == nil { + startFromLastReservedIP = true + } + } + if startFromLastReservedIP { + startIP = a.nextIP(lastReservedIP) + endIP = lastReservedIP + } else { + startIP = a.start + endIP = a.end + } + return startIP, endIP +} diff --git a/plugins/ipam/vz-local/allocator_test.go b/plugins/ipam/vz-local/allocator_test.go new file mode 100644 index 0000000..e1b19ee --- /dev/null +++ b/plugins/ipam/vz-local/allocator_test.go @@ -0,0 +1,355 @@ +// Copyright 2016 CNI 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 main + +import ( + "fmt" + "github.com/containernetworking/cni/pkg/types" + fakestore "github.com/containernetworking/cni/plugins/ipam/host-local/backend/testing" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + "net" +) + +type AllocatorTestCase struct { + subnet string + ipmap map[string]string + expectResult string + lastIP string +} + +func (t AllocatorTestCase) run() (*types.IPConfig, error) { + subnet, err := types.ParseCIDR(t.subnet) + if err != nil { + return nil, err + } + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + } + store := fakestore.NewFakeStore(t.ipmap, net.ParseIP(t.lastIP)) + alloc, err := NewIPAllocator(&conf, store) + if err != nil { + return nil, err + } + res, err := alloc.Get("ID") + if err != nil { + return nil, err + } + + return res, nil +} + +var _ = Describe("host-local ip allocator", func() { + Context("when has free ip", func() { + It("should allocate ips in round robin", func() { + testCases := []AllocatorTestCase{ + // fresh start + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{}, + expectResult: "10.0.0.2", + lastIP: "", + }, + { + subnet: "10.0.0.0/30", + ipmap: map[string]string{}, + expectResult: "10.0.0.2", + lastIP: "", + }, + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{ + "10.0.0.2": "id", + }, + expectResult: "10.0.0.3", + lastIP: "", + }, + // next ip of last reserved ip + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{}, + expectResult: "10.0.0.6", + lastIP: "10.0.0.5", + }, + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{ + "10.0.0.4": "id", + "10.0.0.5": "id", + }, + expectResult: "10.0.0.6", + lastIP: "10.0.0.3", + }, + // round robin to the beginning + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{ + "10.0.0.6": "id", + }, + expectResult: "10.0.0.2", + lastIP: "10.0.0.5", + }, + // lastIP is out of range + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{ + "10.0.0.2": "id", + }, + expectResult: "10.0.0.3", + lastIP: "10.0.0.128", + }, + // wrap around and reserve lastIP + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{ + "10.0.0.2": "id", + "10.0.0.4": "id", + "10.0.0.5": "id", + "10.0.0.6": "id", + }, + expectResult: "10.0.0.3", + lastIP: "10.0.0.3", + }, + } + + for _, tc := range testCases { + res, err := tc.run() + Expect(err).ToNot(HaveOccurred()) + Expect(res.IP.IP.String()).To(Equal(tc.expectResult)) + } + }) + + It("should not allocate the broadcast address", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + alloc, err := NewIPAllocator(&conf, store) + Expect(err).ToNot(HaveOccurred()) + + for i := 1; i < 254; i++ { + res, err := alloc.Get("ID") + Expect(err).ToNot(HaveOccurred()) + // i+1 because the gateway address is skipped + s := fmt.Sprintf("192.168.1.%d/24", i+1) + Expect(s).To(Equal(res.IP.String())) + } + + _, err = alloc.Get("ID") + Expect(err).To(HaveOccurred()) + }) + + It("should allocate RangeStart first", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + RangeStart: net.ParseIP("192.168.1.10"), + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + alloc, err := NewIPAllocator(&conf, store) + Expect(err).ToNot(HaveOccurred()) + + res, err := alloc.Get("ID") + Expect(err).ToNot(HaveOccurred()) + Expect(res.IP.String()).To(Equal("192.168.1.10/24")) + + res, err = alloc.Get("ID") + Expect(err).ToNot(HaveOccurred()) + Expect(res.IP.String()).To(Equal("192.168.1.11/24")) + }) + + It("should allocate RangeEnd but not past RangeEnd", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + RangeEnd: net.ParseIP("192.168.1.5"), + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + alloc, err := NewIPAllocator(&conf, store) + Expect(err).ToNot(HaveOccurred()) + + for i := 1; i < 5; i++ { + res, err := alloc.Get("ID") + Expect(err).ToNot(HaveOccurred()) + // i+1 because the gateway address is skipped + Expect(res.IP.String()).To(Equal(fmt.Sprintf("192.168.1.%d/24", i+1))) + } + + _, err = alloc.Get("ID") + Expect(err).To(HaveOccurred()) + }) + + Context("when requesting a specific IP", func() { + It("must allocate the requested IP", func() { + subnet, err := types.ParseCIDR("10.0.0.0/29") + Expect(err).ToNot(HaveOccurred()) + requestedIP := net.ParseIP("10.0.0.2") + ipmap := map[string]string{} + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + Args: &IPAMArgs{IP: requestedIP}, + } + store := fakestore.NewFakeStore(ipmap, nil) + alloc, _ := NewIPAllocator(&conf, store) + res, err := alloc.Get("ID") + Expect(err).ToNot(HaveOccurred()) + Expect(res.IP.IP.String()).To(Equal(requestedIP.String())) + }) + + It("must return an error when the requested IP is after RangeEnd", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + ipmap := map[string]string{} + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + Args: &IPAMArgs{IP: net.ParseIP("192.168.1.50")}, + RangeEnd: net.ParseIP("192.168.1.20"), + } + store := fakestore.NewFakeStore(ipmap, nil) + alloc, _ := NewIPAllocator(&conf, store) + _, err = alloc.Get("ID") + Expect(err).To(HaveOccurred()) + }) + + It("must return an error when the requested IP is before RangeStart", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + ipmap := map[string]string{} + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + Args: &IPAMArgs{IP: net.ParseIP("192.168.1.3")}, + RangeStart: net.ParseIP("192.168.1.10"), + } + store := fakestore.NewFakeStore(ipmap, nil) + alloc, _ := NewIPAllocator(&conf, store) + _, err = alloc.Get("ID") + Expect(err).To(HaveOccurred()) + }) + }) + + It("RangeStart must be in the given subnet", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + RangeStart: net.ParseIP("10.0.0.1"), + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + _, err = NewIPAllocator(&conf, store) + Expect(err).To(HaveOccurred()) + }) + + It("RangeEnd must be in the given subnet", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + RangeEnd: net.ParseIP("10.0.0.1"), + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + _, err = NewIPAllocator(&conf, store) + Expect(err).To(HaveOccurred()) + }) + + It("RangeEnd must be after RangeStart in the given subnet", func() { + subnet, err := types.ParseCIDR("192.168.1.0/24") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + RangeStart: net.ParseIP("192.168.1.10"), + RangeEnd: net.ParseIP("192.168.1.3"), + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + _, err = NewIPAllocator(&conf, store) + Expect(err).To(HaveOccurred()) + }) + }) + + Context("when out of ips", func() { + It("returns a meaningful error", func() { + testCases := []AllocatorTestCase{ + { + subnet: "10.0.0.0/30", + ipmap: map[string]string{ + "10.0.0.2": "id", + "10.0.0.3": "id", + }, + }, + { + subnet: "10.0.0.0/29", + ipmap: map[string]string{ + "10.0.0.2": "id", + "10.0.0.3": "id", + "10.0.0.4": "id", + "10.0.0.5": "id", + "10.0.0.6": "id", + "10.0.0.7": "id", + }, + }, + } + for _, tc := range testCases { + _, err := tc.run() + Expect(err).To(MatchError("no IP addresses available in network: test")) + } + }) + }) + + Context("when given an invalid subnet", func() { + It("returns a meaningful error", func() { + subnet, err := types.ParseCIDR("192.168.1.0/31") + Expect(err).ToNot(HaveOccurred()) + + conf := IPAMConfig{ + Name: "test", + Type: "host-local", + Subnet: types.IPNet{IP: subnet.IP, Mask: subnet.Mask}, + } + store := fakestore.NewFakeStore(map[string]string{}, net.ParseIP("")) + _, err = NewIPAllocator(&conf, store) + Expect(err).To(HaveOccurred()) + }) + }) +}) diff --git a/plugins/ipam/vz-local/backend/disk/backend.go b/plugins/ipam/vz-local/backend/disk/backend.go new file mode 100644 index 0000000..14dd40d --- /dev/null +++ b/plugins/ipam/vz-local/backend/disk/backend.go @@ -0,0 +1,110 @@ +// Copyright 2015 CNI 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 disk + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" +) + +const lastIPFile = "last_reserved_ip" + +var defaultDataDir = "/var/lib/cni/networks" + +type Store struct { + FileLock + dataDir string +} + +func New(network, dataDir string) (*Store, error) { + if dataDir == "" { + dataDir = defaultDataDir + } + dir := filepath.Join(dataDir, network) + if err := os.MkdirAll(dir, 0644); err != nil { + return nil, err + } + + lk, err := NewFileLock(dir) + if err != nil { + return nil, err + } + return &Store{*lk, dir}, nil +} + +func (s *Store) Reserve(id string, ip net.IP) (bool, error) { + fname := filepath.Join(s.dataDir, ip.String()) + f, err := os.OpenFile(fname, os.O_RDWR|os.O_EXCL|os.O_CREATE, 0644) + if os.IsExist(err) { + return false, nil + } + if err != nil { + return false, err + } + if _, err := f.WriteString(id); err != nil { + f.Close() + os.Remove(f.Name()) + return false, err + } + if err := f.Close(); err != nil { + os.Remove(f.Name()) + return false, err + } + // store the reserved ip in lastIPFile + ipfile := filepath.Join(s.dataDir, lastIPFile) + err = ioutil.WriteFile(ipfile, []byte(ip.String()), 0644) + if err != nil { + return false, err + } + return true, nil +} + +// LastReservedIP returns the last reserved IP if exists +func (s *Store) LastReservedIP() (net.IP, error) { + ipfile := filepath.Join(s.dataDir, lastIPFile) + data, err := ioutil.ReadFile(ipfile) + if err != nil { + return nil, fmt.Errorf("Failed to retrieve last reserved ip: %v", err) + } + return net.ParseIP(string(data)), nil +} + +func (s *Store) Release(ip net.IP) error { + return os.Remove(filepath.Join(s.dataDir, ip.String())) +} + +// N.B. This function eats errors to be tolerant and +// release as much as possible +func (s *Store) ReleaseByID(id string) error { + err := filepath.Walk(s.dataDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() { + return nil + } + data, err := ioutil.ReadFile(path) + if err != nil { + return nil + } + if string(data) == id { + if err := os.Remove(path); err != nil { + return nil + } + } + return nil + }) + return err +} diff --git a/plugins/ipam/vz-local/backend/disk/lock.go b/plugins/ipam/vz-local/backend/disk/lock.go new file mode 100644 index 0000000..7241482 --- /dev/null +++ b/plugins/ipam/vz-local/backend/disk/lock.go @@ -0,0 +1,50 @@ +// Copyright 2015 CNI 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 disk + +import ( + "os" + "syscall" +) + +// FileLock wraps os.File to be used as a lock using flock +type FileLock struct { + f *os.File +} + +// NewFileLock opens file/dir at path and returns unlocked FileLock object +func NewFileLock(path string) (*FileLock, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + + return &FileLock{f}, nil +} + +// Close closes underlying file +func (l *FileLock) Close() error { + return l.f.Close() +} + +// Lock acquires an exclusive lock +func (l *FileLock) Lock() error { + return syscall.Flock(int(l.f.Fd()), syscall.LOCK_EX) +} + +// Unlock releases the lock +func (l *FileLock) Unlock() error { + return syscall.Flock(int(l.f.Fd()), syscall.LOCK_UN) +} diff --git a/plugins/ipam/vz-local/backend/store.go b/plugins/ipam/vz-local/backend/store.go new file mode 100644 index 0000000..82ba869 --- /dev/null +++ b/plugins/ipam/vz-local/backend/store.go @@ -0,0 +1,27 @@ +// Copyright 2015 CNI 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 backend + +import "net" + +type Store interface { + Lock() error + Unlock() error + Close() error + Reserve(id string, ip net.IP) (bool, error) + LastReservedIP() (net.IP, error) + Release(ip net.IP) error + ReleaseByID(id string) error +} diff --git a/plugins/ipam/vz-local/backend/testing/fake_store.go b/plugins/ipam/vz-local/backend/testing/fake_store.go new file mode 100644 index 0000000..f7750ca --- /dev/null +++ b/plugins/ipam/vz-local/backend/testing/fake_store.go @@ -0,0 +1,72 @@ +// Copyright 2015 CNI 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 testing + +import ( + "net" +) + +type FakeStore struct { + ipMap map[string]string + lastReservedIP net.IP +} + +func NewFakeStore(ipmap map[string]string, lastIP net.IP) *FakeStore { + return &FakeStore{ipmap, lastIP} +} + +func (s *FakeStore) Lock() error { + return nil +} + +func (s *FakeStore) Unlock() error { + return nil +} + +func (s *FakeStore) Close() error { + return nil +} + +func (s *FakeStore) Reserve(id string, ip net.IP) (bool, error) { + key := ip.String() + if _, ok := s.ipMap[key]; !ok { + s.ipMap[key] = id + s.lastReservedIP = ip + return true, nil + } + return false, nil +} + +func (s *FakeStore) LastReservedIP() (net.IP, error) { + return s.lastReservedIP, nil +} + +func (s *FakeStore) Release(ip net.IP) error { + delete(s.ipMap, ip.String()) + return nil +} + +func (s *FakeStore) ReleaseByID(id string) error { + toDelete := []string{} + for k, v := range s.ipMap { + if v == id { + toDelete = append(toDelete, k) + } + } + for _, ip := range toDelete { + delete(s.ipMap, ip) + } + return nil +} diff --git a/plugins/ipam/vz-local/config.go b/plugins/ipam/vz-local/config.go new file mode 100644 index 0000000..619f6d8 --- /dev/null +++ b/plugins/ipam/vz-local/config.go @@ -0,0 +1,193 @@ +// Copyright 2015 CNI 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 main + +import ( + "encoding/json" + "fmt" + "net" + + "os" + + "github.com/containernetworking/cni/pkg/types" + // "github.com/containernetworking/cni/ipam/vz-local/custom" +) + +// IPAMConfig represents the IP related network configuration. +type IPAMConfig struct { + Name string + Type string `json:"type"` + RangeStart net.IP `json:"rangeStart"` + RangeEnd net.IP `json:"rangeEnd"` + Subnet types.IPNet `json:"subnet"` + Gateway net.IP `json:"gateway"` + Routes []types.Route `json:"routes"` + DataDir string `json:"dataDir"` + Args *IPAMArgs `json:"-"` +} + +type IPAMArgs struct { + types.CommonArgs + IP net.IP `json:"ip,omitempty"` + UPLINK types.UnmarshallableString `json:"uplink,omitempty"` +} + +/* + * Add structs needed to parse labels + */ +type Args struct { + Mesos Mesos `json:"org.apache.mesos,omitempty"` +} + +type Mesos struct { + NetworkInfo NetworkInfo `json:"network_info"` +} + +type NetworkInfo struct { + Name string `json:"name"` + Labels struct { + Labels []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"labels,omitempty"` + } `json:"labels,omitempty"` +} + +// NetConf describes a network. +type NetConf struct { + CNIVersion string `json:"cniVersion,omitempty"` + + Name string `json:"name,omitempty"` + Type string `json:"type,omitempty"` + IPAM *IPAMConfig `json:"ipam"` + DNS types.DNS `json:"dns"` + ARGS *Args `json:"args,omitempty"` + // ARGS Args `json:"args"` +} + +// NewIPAMConfig creates a NetworkConfig from the given network name. +func LoadIPAMConfig(bytes []byte, args string) (*IPAMConfig, error) { + //n := Net{} + n := NetConf{} + if err := json.Unmarshal(bytes, &n); err != nil { + return nil, err + } + + if args != "" { + n.IPAM.Args = &IPAMArgs{} + err := types.LoadArgs(args, n.IPAM.Args) + if err != nil { + return nil, err + } + } + + if n.IPAM == nil { + return nil, fmt.Errorf("IPAM config missing 'ipam' key") + } + + // Copy net name into IPAM so not to drag Net struct around + n.IPAM.Name = n.Name + +/* + * Begin + */ + println("Debug Getenv - Start") + + mccval := os.Getenv("MCCVAL") + println("mccval is: ", mccval) + println("Debug Getenv- End") +/* + * End + */ + +/* + * Begin + */ + println("Debug - Start") + labels := map[string]string{} + + // println("initial:") + // fmt.Println("Initial Map:", labels) + // println(":initial") + + if args != "" { + if n.ARGS != nil { + println("Bingo") + //n.IPAM.Args = &IPAMArgs{} + //n.ARGS.Mesos = &Mesos{} + + // fmt.Printf("NetConf Struct type is: ") + // fmt.Printf("%T ", n) + // fmt.Printf("NetConf Struct type is: ") + // fmt.Printf("%+v", n) + //myargs := &n.Args + // m := &myargs.Mesos{} + //fmt.Printf("myargs Struct is: %T = %+v", myargs, myargs) + //fmt.Printf("Mesos Struct is: %T = %+v", m, m) + + //n.ARGS = &Args{} + //fmt.Printf("Args Struct type is: ") + //fmt.Printf("T", n.ARGS) + //fmt.Printf("Args Struct value is: ") + //fmt.Printf("%+v", n.ARGS) + + for k, label := range n.ARGS.Mesos.NetworkInfo.Labels.Labels { + labels[label.Key] = label.Value + println("Map k (for)", k) + println("Map k (for)", k, label.Key, label.Value) + // println("Map (for)", labels) + // fmt.Println("Map: ", label) + } + // println("Map (final)", labels) + //fmt.Println("Final Map (Println)", labels) + //fmt.Printf("Final Map (Printf) %v", labels) + } + } + println("Debug - End") + + for key, value := range labels { + // fmt.Println("Key:", key, "Value:", value) + println("Key:", key, "Value:", value) + } + + //fmt.Println("done:", n.ARGS.Mesos.NetworkInfo.Labels.Labels) + if n.ARGS != nil { + println("done: Net Name: ", n.ARGS.Mesos.NetworkInfo.Name) + println("done: IPAM Name: ", n.IPAM.Name) + println("done: Labels2: ", n.ARGS.Mesos.NetworkInfo.Labels.Labels) + args_ip := n.IPAM.Args.IP + uplink := n.IPAM.Args.UPLINK + println("MikeC: n.IPAM.IP is:", args_ip) + println("MikeC: n.IPAM.UPLINK is:", uplink) + } + + staticIP, found := labels["StaticIP"] + if found { + println("StaticIP is: ", staticIP) + } + + bull, found := labels["bull"] + if !found { + println("Hard to believe, but bull not found") + } else { + println("Found: ", bull) + } + +/* + * End + */ + + return n.IPAM, nil +} diff --git a/plugins/ipam/vz-local/custom/hack-types-dot-go b/plugins/ipam/vz-local/custom/hack-types-dot-go new file mode 100644 index 0000000..90c19d6 --- /dev/null +++ b/plugins/ipam/vz-local/custom/hack-types-dot-go @@ -0,0 +1,32 @@ + +type Args struct { + Mesos Mesos `json:"org.apache.mesos,omitempty"` +} + +type Mesos struct { + NetworkInfo NetworkInfo `json:"network_info"` +} + +type NetworkInfo struct { + Name string `json:"name"` + Labels struct { + Labels []struct { + Key string `json:"key"` + Value string `json:"value"` + } `json:"labels,omitempty"` + } `json:"labels,omitempty"` +} + +// NetConf describes vz custom ipam network +type NetConf struct { + CNIVersion string `json:"cniVersion,omitempty"` + + Name string `json:"name"` + Type string `json:"type"` + IPAM struct { + Type string `json:"type,omitempty"` + } `json:"ipam,omitempty"` + DNS DNS `json:"dns"` + Args Args `json:"args"` +} + diff --git a/plugins/ipam/vz-local/host_local_suite_test.go b/plugins/ipam/vz-local/host_local_suite_test.go new file mode 100644 index 0000000..4dc89a0 --- /dev/null +++ b/plugins/ipam/vz-local/host_local_suite_test.go @@ -0,0 +1,27 @@ +// Copyright 2016 CNI 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 main_test + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "testing" +) + +func TestHostLocal(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "HostLocal Suite") +} diff --git a/plugins/ipam/vz-local/host_local_test.go b/plugins/ipam/vz-local/host_local_test.go new file mode 100644 index 0000000..41dba71 --- /dev/null +++ b/plugins/ipam/vz-local/host_local_test.go @@ -0,0 +1,92 @@ +// Copyright 2016 CNI 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 main + +import ( + "fmt" + "io/ioutil" + "net" + "os" + "path/filepath" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/testutils" + "github.com/containernetworking/cni/pkg/types" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("host-local Operations", func() { + It("allocates and releases an address with ADD/DEL", func() { + const ifname string = "eth0" + const nspath string = "/some/where" + + tmpDir, err := ioutil.TempDir("", "host_local_artifacts") + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(tmpDir) + + conf := fmt.Sprintf(`{ + "cniVersion": "0.2.0", + "name": "mynet", + "type": "ipvlan", + "master": "foo0", + "ipam": { + "type": "host-local", + "subnet": "10.1.2.0/24", + "dataDir": "%s" + } +}`, tmpDir) + + args := &skel.CmdArgs{ + ContainerID: "dummy", + Netns: nspath, + IfName: ifname, + StdinData: []byte(conf), + } + + // Allocate the IP + result, err := testutils.CmdAddWithResult(nspath, ifname, func() error { + return cmdAdd(args) + }) + Expect(err).NotTo(HaveOccurred()) + + expectedAddress, err := types.ParseCIDR("10.1.2.2/24") + Expect(err).NotTo(HaveOccurred()) + expectedAddress.IP = expectedAddress.IP.To16() + Expect(result.IP4.IP).To(Equal(*expectedAddress)) + + Expect(result.IP4.Gateway).To(Equal(net.ParseIP("10.1.2.1"))) + + ipFilePath := filepath.Join(tmpDir, "mynet", "10.1.2.2") + contents, err := ioutil.ReadFile(ipFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(contents)).To(Equal("dummy")) + + lastFilePath := filepath.Join(tmpDir, "mynet", "last_reserved_ip") + contents, err = ioutil.ReadFile(lastFilePath) + Expect(err).NotTo(HaveOccurred()) + Expect(string(contents)).To(Equal("10.1.2.2")) + + // Release the IP + err = testutils.CmdDelWithResult(nspath, ifname, func() error { + return cmdDel(args) + }) + Expect(err).NotTo(HaveOccurred()) + + _, err = os.Stat(ipFilePath) + Expect(err).To(HaveOccurred()) + }) +}) diff --git a/plugins/ipam/vz-local/main.go b/plugins/ipam/vz-local/main.go new file mode 100644 index 0000000..19a3180 --- /dev/null +++ b/plugins/ipam/vz-local/main.go @@ -0,0 +1,75 @@ +// Copyright 2015 CNI 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 main + +import ( + "github.com/containernetworking/cni/plugins/ipam/host-local/backend/disk" + + "github.com/containernetworking/cni/pkg/skel" + "github.com/containernetworking/cni/pkg/types" + "github.com/containernetworking/cni/pkg/version" +) + +func main() { + skel.PluginMain(cmdAdd, cmdDel, version.Legacy) +} + +func cmdAdd(args *skel.CmdArgs) error { + ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args) + if err != nil { + return err + } + + store, err := disk.New(ipamConf.Name, ipamConf.DataDir) + if err != nil { + return err + } + defer store.Close() + + allocator, err := NewIPAllocator(ipamConf, store) + if err != nil { + return err + } + + ipConf, err := allocator.Get(args.ContainerID) + if err != nil { + return err + } + + r := &types.Result{ + IP4: ipConf, + } + return r.Print() +} + +func cmdDel(args *skel.CmdArgs) error { + ipamConf, err := LoadIPAMConfig(args.StdinData, args.Args) + if err != nil { + return err + } + + store, err := disk.New(ipamConf.Name, ipamConf.DataDir) + if err != nil { + return err + } + defer store.Close() + + allocator, err := NewIPAllocator(ipamConf, store) + if err != nil { + return err + } + + return allocator.Release(args.ContainerID) +} -- 2.44.0