Skip to content

Commit

Permalink
Merge pull request #32 from cmingxu/ipam-new
Browse files Browse the repository at this point in the history
Ipam new
  • Loading branch information
upccup committed Nov 21, 2016
2 parents b0db39c + 517e4e1 commit f8f6310
Show file tree
Hide file tree
Showing 12 changed files with 1,070 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@ coverage-all.out
coverage.out
coverage.html
.bolt.db
.bolt-foobar.db
141 changes: 141 additions & 0 deletions api/router/ipam/ipam.go
@@ -0,0 +1,141 @@
package ipam

import (
"encoding/json"
"errors"
"net/http"

"github.com/Dataman-Cloud/swan/api/utils"
ipamanger "github.com/Dataman-Cloud/swan/ipam"
)

func (r *Router) AllocateIP(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

if err := req.ParseForm(); err != nil {
return err
}

ipStr := req.Form.Get("ip")
if ipStr == "" {
return errors.New("no ip specified")
}

ip, err := r.ipam.AllocateIp(ipamanger.IP{Ip: ipStr})
if err != nil {
return err
}

return json.NewEncoder(w).Encode(ip)
}

func (r *Router) AllocateNextAvailable(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

ip, err := r.ipam.AllocateNextAvailableIP()
if err != nil {
return err
}

return json.NewEncoder(w).Encode(ip)
}

func (r *Router) ListAvailableIps(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

list, err := r.ipam.IPsAvailable()
if err != nil {
return err
}

return json.NewEncoder(w).Encode(list)
}

func (r *Router) ListAllocatedIps(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

list, err := r.ipam.IPsAllocated()
if err != nil {
return err
}

return json.NewEncoder(w).Encode(list)
}

func (r *Router) ReleaseIP(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

if err := req.ParseForm(); err != nil {
return err
}

var param struct {
IP string `json:"ip"`
}

decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&param); err != nil {
return err
}

err := r.ipam.Release(ipamanger.IP{Ip: param.IP})
if err != nil {
return err
}

return nil
}

func (r *Router) RefillIPs(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

if err := req.ParseForm(); err != nil {
return err
}

var param struct {
IPs []string `json:"ips"`
}

var ips []ipamanger.IP
decoder := json.NewDecoder(req.Body)
if err := decoder.Decode(&param); err != nil {
return err
}

for _, ipStr := range param.IPs {
ips = append(ips, ipamanger.IP{Ip: ipStr})
}

err := r.ipam.Refill(ips)
if err != nil {
return err
}

return nil
}

func (r *Router) ListIPs(w http.ResponseWriter, req *http.Request) error {
if err := utils.CheckForJSON(req); err != nil {
return err
}

list, err := r.ipam.AllIPs()
if err != nil {
return err
}

return json.NewEncoder(w).Encode(list)
}
37 changes: 37 additions & 0 deletions api/router/ipam/routes.go
@@ -0,0 +1,37 @@
package ipam

import (
"github.com/Dataman-Cloud/swan/api/router"
manager "github.com/Dataman-Cloud/swan/ipam"
)

type Router struct {
routes []*router.Route
ipam *manager.IPAM
}

// NewRouter initializes a new ipam router.
func NewRouter(manager *manager.IPAM) *Router {
r := &Router{
ipam: manager,
}

r.initRoutes()
return r
}

func (r *Router) Routes() []*router.Route {
return r.routes
}

func (r *Router) initRoutes() {
r.routes = []*router.Route{
router.NewRoute("GET", "/v1/ipam/allocate_randomly", r.AllocateNextAvailable),
router.NewRoute("GET", "/v1/ipam/allocated_ips", r.ListAllocatedIps),
router.NewRoute("GET", "/v1/ipam/available_ips", r.ListAvailableIps),
router.NewRoute("POST", "/v1/ipam/release", r.ReleaseIP),
router.NewRoute("GET", "/v1/ipam/allocate", r.AllocateIP),
router.NewRoute("POST", "/v1/ipam/ips", r.RefillIPs),
router.NewRoute("GET", "/v1/ipam/ips", r.ListIPs),
}
}
37 changes: 37 additions & 0 deletions design-docs/ipam.md
@@ -0,0 +1,37 @@
# IP Address Manager
The IPAM for Swan is supposed to manage lifecycle of a predefined group
of IPs, all IP addresses should be avaliable within the same layer 2
subnet as well as the hosts, all of them are reserved for containers,
each container could be assigned a unique ip, with underlaying Macvlan
bridge created by docker.

## This is not a docker plugin
The initial thought would be make this as a docker plugin like DHCP
IPAM, so docker daemon could reach the IPAM remotely to where it stay in the Swan
managers. But the truth is that as we have our own scheduler by default so this
IPAM was not intend to run in standlone mode without scheduler, as though the only consumer of
the IPAM would be the schdueler itself, it might be better choice make
the IPAM not a plugin but part of scheduler which can access both from
HTTP API and call directly.

## How to initialize the IP list pool
IP list pool supposed to be entered mannuly through HTTP API, which
each ip should be unique and accessible within the same layer 2 subnet.

## Lifecycle of a ip

* `avaliable` avaliable to be allocate to a container
* `reserved` reserved, should not allocated to any container
* `allocated` currently used by a container
* `releasing` released but not avaliable soon, will be turn into
avaliable state after certain time periods

## How to interact with IPAM, the APIs

* `list` avaliable ips, no matter what state they are
* `initialize` the ip pool
* `empty` the ip pool
* `allocate` the ip from pool
* `release` a ip back to the pool


3 changes: 3 additions & 0 deletions examplejson/example-ipam.json
@@ -0,0 +1,3 @@
{
"ips": ["192.168.1.2", "192.168.1.3", "192.168.1.4", "192.168.1.5", "192.168.1.6", "192.168.1.7", "192.168.1.8", "192.168.1.9", "192.168.1.10", "192.168.1.11", "192.168.1.12", "192.168.1.13", "192.168.1.14", "192.168.1.15", "192.168.1.16", "192.168.1.17", "192.168.1.18", "192.168.1.19", "192.168.1.20", "192.168.1.21", "192.168.1.22", "192.168.1.23", "192.168.1.24", "192.168.1.25", "192.168.1.26", "192.168.1.27", "192.168.1.28", "192.168.1.29", "192.168.1.30", "192.168.1.31", "192.168.1.32", "192.168.1.33", "192.168.1.34", "192.168.1.35", "192.168.1.36", "192.168.1.37", "192.168.1.38", "192.168.1.39", "192.168.1.40", "192.168.1.41", "192.168.1.42", "192.168.1.43", "192.168.1.44", "192.168.1.45", "192.168.1.46", "192.168.1.47", "192.168.1.48", "192.168.1.49", "192.168.1.50", "192.168.1.51", "192.168.1.52", "192.168.1.53", "192.168.1.54", "192.168.1.55", "192.168.1.56", "192.168.1.57", "192.168.1.58", "192.168.1.59", "192.168.1.60", "192.168.1.61", "192.168.1.62", "192.168.1.63", "192.168.1.64", "192.168.1.65", "192.168.1.66", "192.168.1.67", "192.168.1.68", "192.168.1.69", "192.168.1.70", "192.168.1.71", "192.168.1.72", "192.168.1.73", "192.168.1.74", "192.168.1.75", "192.168.1.76", "192.168.1.77", "192.168.1.78", "192.168.1.79", "192.168.1.80", "192.168.1.81", "192.168.1.82", "192.168.1.83", "192.168.1.84", "192.168.1.85", "192.168.1.86", "192.168.1.87", "192.168.1.88", "192.168.1.89", "192.168.1.90", "192.168.1.91", "192.168.1.92", "192.168.1.93", "192.168.1.94", "192.168.1.95", "192.168.1.96", "192.168.1.97", "192.168.1.98", "192.168.1.99", "192.168.1.100", "192.168.1.101", "192.168.1.102", "192.168.1.103", "192.168.1.104", "192.168.1.105", "192.168.1.106", "192.168.1.107", "192.168.1.108", "192.168.1.109", "192.168.1.110", "192.168.1.111", "192.168.1.112", "192.168.1.113", "192.168.1.114", "192.168.1.115", "192.168.1.116", "192.168.1.117", "192.168.1.118", "192.168.1.119", "192.168.1.120", "192.168.1.121", "192.168.1.122", "192.168.1.123", "192.168.1.124", "192.168.1.125", "192.168.1.126", "192.168.1.127", "192.168.1.128", "192.168.1.129", "192.168.1.130", "192.168.1.131", "192.168.1.132", "192.168.1.133", "192.168.1.134", "192.168.1.135", "192.168.1.136", "192.168.1.137", "192.168.1.138", "192.168.1.139", "192.168.1.140", "192.168.1.141", "192.168.1.142", "192.168.1.143", "192.168.1.144", "192.168.1.145", "192.168.1.146", "192.168.1.147", "192.168.1.148", "192.168.1.149", "192.168.1.150", "192.168.1.151", "192.168.1.152", "192.168.1.153", "192.168.1.154", "192.168.1.155", "192.168.1.156", "192.168.1.157", "192.168.1.158", "192.168.1.159", "192.168.1.160", "192.168.1.161", "192.168.1.162", "192.168.1.163", "192.168.1.164", "192.168.1.165", "192.168.1.166", "192.168.1.167", "192.168.1.168", "192.168.1.169", "192.168.1.170", "192.168.1.171", "192.168.1.172", "192.168.1.173", "192.168.1.174", "192.168.1.175", "192.168.1.176", "192.168.1.177", "192.168.1.178", "192.168.1.179", "192.168.1.180", "192.168.1.181", "192.168.1.182", "192.168.1.183", "192.168.1.184", "192.168.1.185", "192.168.1.186", "192.168.1.187", "192.168.1.188", "192.168.1.189", "192.168.1.190", "192.168.1.191", "192.168.1.192", "192.168.1.193", "192.168.1.194", "192.168.1.195", "192.168.1.196", "192.168.1.197", "192.168.1.198", "192.168.1.199", "192.168.1.200", "192.168.1.201", "192.168.1.202", "192.168.1.203", "192.168.1.204", "192.168.1.205", "192.168.1.206", "192.168.1.207", "192.168.1.208", "192.168.1.209", "192.168.1.210", "192.168.1.211", "192.168.1.212", "192.168.1.213", "192.168.1.214", "192.168.1.215", "192.168.1.216", "192.168.1.217", "192.168.1.218", "192.168.1.219", "192.168.1.220", "192.168.1.221", "192.168.1.222", "192.168.1.223", "192.168.1.224", "192.168.1.225", "192.168.1.226", "192.168.1.227", "192.168.1.228", "192.168.1.229", "192.168.1.230", "192.168.1.231", "192.168.1.232", "192.168.1.233", "192.168.1.234", "192.168.1.235", "192.168.1.236", "192.168.1.237", "192.168.1.238", "192.168.1.239", "192.168.1.240", "192.168.1.241", "192.168.1.242", "192.168.1.243", "192.168.1.244", "192.168.1.245", "192.168.1.246", "192.168.1.247", "192.168.1.248", "192.168.1.249", "192.168.1.250", "192.168.1.251", "192.168.1.252", "192.168.1.253", "192.168.1.254"]
}
105 changes: 105 additions & 0 deletions ipam/ip.go
@@ -0,0 +1,105 @@
package ipam

import (
"errors"
"fmt"
"net"
"strconv"
"strings"
"time"
)

const (
IP_STATE_AVAILABLE = "available"
IP_STATE_ALLOCATED = "allocated"
)

var (
ErrParseIp = errors.New("ip parse IP")
ErrIsMultiCastIP = errors.New("ip is a multicast IP")
ErrIsLoopbackIP = errors.New("ip is a loopback IP")
ErrIsUnspecified = errors.New("ip is unspecified")
ErrIsLinkLocalUnicast = errors.New("ip is link local unicast")
)

// see `https://golang.org/src/net/ip.go`

type IP struct {
Ip string `json:"Ip"`
State string `json:"State"`
ReleaseAt time.Time `json:"ReleaseAt"`
TaskId string `json:"TaskId"`
}

func NewIpFromIp(ip string) (IP, error) {
if err := validIp(ip); err != nil {
return IP{}, err
}

return IP{
Ip: ip,
State: IP_STATE_AVAILABLE,
}, nil
}

func validIp(ipString string) error {
ip := net.ParseIP(ipString)

if ip == nil {
return ErrParseIp
}

if ip.IsUnspecified() {
return ErrIsUnspecified
}

if ip.IsLoopback() {
return ErrIsLoopbackIP
}

if ip.IsLinkLocalUnicast() {
return ErrIsLinkLocalUnicast
}

if ip.IsMulticast() {
return ErrIsMultiCastIP
}

return nil
}

func (ip IP) ToIP() net.IP {
ipv4 := net.ParseIP(ip.Ip)
return ipv4
}

func (ip IP) ToString() string {
return fmt.Sprintf("ip<%s> state<%s> taskId<%s>", ip.Ip, ip.State, ip.TaskId)
}

func (ip IP) ToInteger() int64 {
ip4 := ip.ToIP().To4()
bin := make([]string, len(ip4))
for i, v := range ip4 {
bin[i] = fmt.Sprintf("%08b", v)
}
i, _ := strconv.ParseInt(strings.Join(bin, ""), 2, 64)

return i
}

func (ip IP) Key() string {
return ip.Ip
}

type IPList []IP

func (s IPList) Len() int {
return len(s)
}
func (s IPList) Swap(i, j int) {
s[i], s[j] = s[j], s[i]
}
func (s IPList) Less(i, j int) bool {
return s[i].ToInteger() < s[j].ToInteger()
}
64 changes: 64 additions & 0 deletions ipam/ip_test.go
@@ -0,0 +1,64 @@
package ipam

import (
"net"
"sort"
"strconv"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIpToString(t *testing.T) {
ip := IP{Ip: "127.0.0.1", State: IP_STATE_AVAILABLE, TaskId: "task-id"}
assert.Equal(t, ip.ToString(), "ip<127.0.0.1> state<available> taskId<task-id>")
}

func TestIpKey(t *testing.T) {
ip := IP{Ip: "127.0.0.1", State: IP_STATE_AVAILABLE, TaskId: "task-id"}
assert.Equal(t, "127.0.0.1", ip.Key())
}

func TestToIP(t *testing.T) {
ip := IP{Ip: "127.0.0.1", State: IP_STATE_AVAILABLE, TaskId: "task-id"}
ipExpected := net.ParseIP("127.0.0.1")
assert.Equal(t, ipExpected, ip.ToIP())
}

func TestToInteger(t *testing.T) {
ip := IP{Ip: "127.0.0.1", State: IP_STATE_AVAILABLE, TaskId: "task-id"}
i, _ := strconv.ParseInt("01111111000000000000000000000001", 2, 64)
assert.Equal(t, i, ip.ToInteger())
}

func TestIPListSort(t *testing.T) {
ip1 := IP{Ip: "127.0.0.1", State: IP_STATE_AVAILABLE, TaskId: "task-id"}
ip2 := IP{Ip: "127.0.0.2", State: IP_STATE_AVAILABLE, TaskId: "task-id"}

ips := IPList([]IP{ip2, ip1})
assert.Equal(t, 2, len(ips))
assert.Equal(t, ip2, ips[0])
sort.Sort(ips)
assert.Equal(t, ip1, ips[0])
}

func TestValidIp(t *testing.T) {
nilIP := "foobar"
assert.Equal(t, ErrParseIp, validIp(nilIP))

unspeficiedIP := "0.0.0.0"
assert.Equal(t, ErrIsUnspecified, validIp(unspeficiedIP))

loobackIP := "127.0.0.1"
assert.Equal(t, ErrIsLoopbackIP, validIp(loobackIP))

linkLocalUnicast := "169.254.1.1"
assert.Equal(t, ErrIsLinkLocalUnicast, validIp(linkLocalUnicast))

multicast := "224.0.0.0"
assert.Equal(t, ErrIsMultiCastIP, validIp(multicast))

validIpStr := "192.168.1.2"
assert.Nil(t, validIp(validIpStr))

}

0 comments on commit f8f6310

Please sign in to comment.