├── docs ├── _config.yml └── index.md ├── client ├── README.md ├── client_suite_test.go ├── leases.go ├── pools.go ├── subnets.go ├── reservations.go ├── client.go └── client_test.go ├── Dockerfile ├── controllers ├── helpers │ ├── helpers_suite_test.go │ ├── media_type.go │ ├── errors_test.go │ ├── media_type_test.go │ ├── errors.go │ ├── renderers.go │ └── renderers_test.go ├── leases.go ├── pools.go ├── subnets.go ├── reservations.go ├── controllers_suite_test.go ├── pools_test.go ├── subnets_test.go └── reservations_test.go ├── resources ├── factory │ ├── factory_suite_test.go │ ├── factory.go │ └── factory_test.go ├── resources_suite_test.go ├── pools.go ├── leases.go ├── subnets.go ├── pools_test.go ├── reservations.go ├── subnets_test.go ├── pool.go ├── reservations_test.go ├── reservation.go ├── lease.go ├── subnet.go ├── pool_test.go ├── subnet_test.go └── reservation_test.go ├── glide.yaml ├── interfaces ├── ipam.go ├── leases.go ├── pools.go ├── subnets.go ├── reservations.go └── resource.go ├── models ├── pool.go ├── reservation.go ├── subnet.go └── lease.go ├── .gitignore ├── docker-compose.yml ├── ipam ├── ipam.go ├── leases.go ├── pools.go ├── reservations.go └── subnets.go ├── glide.lock ├── Makefile ├── main.go ├── README.md ├── .travis.yml └── LICENSE /docs/_config.yml: -------------------------------------------------------------------------------- 1 | theme: minima 2 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # Golang REST client for RackHD ipam 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rackhd/golang:1.7.1-wheezy 2 | 3 | ADD ./bin/ipam /go/bin/ipam 4 | 5 | ENTRYPOINT ["/go/bin/ipam"] 6 | -------------------------------------------------------------------------------- /client/client_suite_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestClient(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Client Suite") 13 | } 14 | -------------------------------------------------------------------------------- /controllers/helpers/helpers_suite_test.go: -------------------------------------------------------------------------------- 1 | package helpers_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestHelpers(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Helpers Suite") 13 | } 14 | -------------------------------------------------------------------------------- /resources/factory/factory_suite_test.go: -------------------------------------------------------------------------------- 1 | package factory_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestFactory(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Factory Suite") 13 | } 14 | -------------------------------------------------------------------------------- /resources/resources_suite_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestResources(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Resources Suite") 13 | } 14 | -------------------------------------------------------------------------------- /glide.yaml: -------------------------------------------------------------------------------- 1 | package: github.com/RackHD/ipam 2 | import: 3 | - package: github.com/gorilla/handlers 4 | version: ^1.1.0 5 | - package: github.com/gorilla/mux 6 | version: ^1.1.0 7 | - package: github.com/hashicorp/go-cleanhttp 8 | - package: gopkg.in/mgo.v2 9 | subpackages: 10 | - bson 11 | -------------------------------------------------------------------------------- /interfaces/ipam.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // Ipam interface is a collection of interfaces defined to support each 4 | // area of business logic. As new interfaces are defined they should be 5 | // added to the Ipam interface to extend it's capabilities. 6 | type Ipam interface { 7 | Pools 8 | Subnets 9 | Reservations 10 | Leases 11 | } 12 | -------------------------------------------------------------------------------- /interfaces/leases.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/RackHD/ipam/models" 4 | 5 | // Leases interface defines the methods for implementing Lease related business logic. 6 | type Leases interface { 7 | GetLeases(string) ([]models.Lease, error) 8 | GetLease(string) (models.Lease, error) 9 | UpdateLease(models.Lease) error 10 | } 11 | -------------------------------------------------------------------------------- /models/pool.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gopkg.in/mgo.v2/bson" 4 | 5 | // Pool is a mgo model representing a collection of Subnet resources. 6 | type Pool struct { 7 | ID bson.ObjectId `bson:"_id"` 8 | Name string `bson:"name"` 9 | Tags []string `bson:"tags"` 10 | Metadata interface{} `bson:"metadata"` 11 | } 12 | -------------------------------------------------------------------------------- /interfaces/pools.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/RackHD/ipam/models" 4 | 5 | // Pools interface defines the methods for implementing Pool related business logic. 6 | type Pools interface { 7 | GetPools() ([]models.Pool, error) 8 | GetPool(string) (models.Pool, error) 9 | CreatePool(models.Pool) error 10 | UpdatePool(models.Pool) error 11 | DeletePool(string) error 12 | } 13 | -------------------------------------------------------------------------------- /interfaces/subnets.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/RackHD/ipam/models" 4 | 5 | // Subnets interface defines the methods for implementing Subnet related business logic. 6 | type Subnets interface { 7 | GetSubnets(string) ([]models.Subnet, error) 8 | GetSubnet(string) (models.Subnet, error) 9 | CreateSubnet(models.Subnet) error 10 | UpdateSubnet(models.Subnet) error 11 | DeleteSubnet(string) error 12 | } 13 | -------------------------------------------------------------------------------- /models/reservation.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gopkg.in/mgo.v2/bson" 4 | 5 | // Reservation is a mgo model representing a collection of Subnet resources. 6 | type Reservation struct { 7 | ID bson.ObjectId `bson:"_id"` 8 | Name string `bson:"name"` 9 | Tags []string `bson:"tags"` 10 | Metadata interface{} `bson:"metadata"` 11 | Subnet bson.ObjectId `bson:"subnet,omitempty"` 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | *.coverprofile 26 | 27 | bin 28 | vendor 29 | -------------------------------------------------------------------------------- /interfaces/reservations.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import "github.com/RackHD/ipam/models" 4 | 5 | // Reservations interface defines the methods for implementing Reservation related business logic. 6 | type Reservations interface { 7 | GetReservations(string) ([]models.Reservation, error) 8 | GetReservation(string) (models.Reservation, error) 9 | CreateReservation(models.Reservation) error 10 | UpdateReservation(models.Reservation) error 11 | DeleteReservation(string) error 12 | } 13 | -------------------------------------------------------------------------------- /models/subnet.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gopkg.in/mgo.v2/bson" 4 | 5 | // Subnet is a mgo model representing a collection of Subnet resources. 6 | type Subnet struct { 7 | ID bson.ObjectId `bson:"_id"` 8 | Name string `bson:"name"` 9 | Tags []string `bson:"tags"` 10 | Metadata interface{} `bson:"metadata"` 11 | Pool bson.ObjectId `bson:"pool,omitempty"` 12 | Start bson.Binary `bson:"start"` 13 | End bson.Binary `bson:"end"` 14 | } 15 | -------------------------------------------------------------------------------- /models/lease.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "gopkg.in/mgo.v2/bson" 4 | 5 | // Lease is a mgo model representing a collection of Subnet resources. 6 | type Lease struct { 7 | ID bson.ObjectId `bson:"_id"` 8 | Name string `bson:"name"` 9 | Tags []string `bson:"tags"` 10 | Metadata interface{} `bson:"metadata"` 11 | Subnet bson.ObjectId `bson:"subnet,omitempty"` 12 | Reservation bson.ObjectId `bson:"reservation,omitempty"` 13 | Address bson.Binary `bson:"address"` 14 | } 15 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | services: 3 | mongodb: 4 | image: "mongo:latest" 5 | container_name: "ipam_mongo" 6 | hostname: "ipam_mongo" 7 | expose: 8 | - "27017" 9 | 10 | ipam: 11 | build: 12 | context: . 13 | dockerfile: Dockerfile 14 | image: "rackhd/ipam:latest" 15 | container_name: "ipam" 16 | hostname: "ipam" 17 | ports: 18 | - "8000:8000" 19 | command: "-mongo ipam_mongo:27017" 20 | depends_on: 21 | - mongodb 22 | 23 | -------------------------------------------------------------------------------- /ipam/ipam.go: -------------------------------------------------------------------------------- 1 | package ipam 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | ) 6 | 7 | // IpamDatabase is the name of the Mongo database used to store IPAM models. 8 | const IpamDatabase string = "ipam" 9 | 10 | // Ipam is an object which implements the IPAM business logic interface. 11 | type Ipam struct { 12 | session *mgo.Session 13 | } 14 | 15 | // NewIpam returns a new Ipam object. 16 | func NewIpam(session *mgo.Session) (*Ipam, error) { 17 | // This is starting us off with an object to play with by default. 18 | return &Ipam{ 19 | session: session, 20 | }, nil 21 | } 22 | -------------------------------------------------------------------------------- /glide.lock: -------------------------------------------------------------------------------- 1 | hash: bd5079f55eccc49a9cb23d2c571ac41b7e196415050baff6a7fb53631e558ae6 2 | updated: 2016-11-22T13:38:19.705256414-05:00 3 | imports: 4 | - name: github.com/gorilla/context 5 | version: 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 6 | - name: github.com/gorilla/handlers 7 | version: ee54c7b44cab12289237fb8631314790076e728b 8 | - name: github.com/gorilla/mux 9 | version: 0eeaf8392f5b04950925b8a69fe70f110fa7cbfc 10 | - name: github.com/hashicorp/go-cleanhttp 11 | version: ad28ea4487f05916463e2423a55166280e8254b5 12 | - name: gopkg.in/mgo.v2 13 | version: 3f83fa5005286a7fe593b055f0d7771a7dce4655 14 | subpackages: 15 | - bson 16 | - internal/json 17 | - internal/sasl 18 | - internal/scram 19 | -------------------------------------------------------------------------------- /controllers/helpers/media_type.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "mime" 5 | "regexp" 6 | ) 7 | 8 | // MediaType represents a set of fields useful to our HTTP routers. 9 | type MediaType struct { 10 | Type string 11 | Version string 12 | } 13 | 14 | // NewMediaType parses an HTTP Accept/Content-Type header and returns a MediaType 15 | // struct with key fields extracted for use in our HTTP routers. 16 | func NewMediaType(requested string) (MediaType, error) { 17 | media, parameters, err := mime.ParseMediaType(requested) 18 | if err != nil { 19 | return MediaType{}, err 20 | } 21 | 22 | regex, err := regexp.Compile(`\+.+`) 23 | if err != nil { 24 | return MediaType{}, err 25 | } 26 | 27 | mediaType := MediaType{ 28 | Type: regex.ReplaceAllString(media, ""), 29 | Version: parameters["version"], 30 | } 31 | 32 | return mediaType, nil 33 | } 34 | -------------------------------------------------------------------------------- /interfaces/resource.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // ResourceCreator is a function signature used for requesting a versioned resource. 4 | type ResourceCreator func(version string) (Resource, error) 5 | 6 | // ResourceMarshaler marshals a given object to it's internal representation. 7 | type ResourceMarshaler interface { 8 | Marshal(interface{}) error 9 | } 10 | 11 | // ResourceUnmarshaler marshals the internal resource representation to a given object (typically a model). 12 | type ResourceUnmarshaler interface { 13 | Unmarshal() (interface{}, error) 14 | } 15 | 16 | // ResourceVersioner provides methods for obtaining information about a particular resource to be used in 17 | // content negotiation in HTTP handlers. 18 | type ResourceVersioner interface { 19 | Type() string 20 | Version() string 21 | } 22 | 23 | // Resource is a composition of interfaces required to represent a resource. 24 | type Resource interface { 25 | ResourceMarshaler 26 | ResourceUnmarshaler 27 | ResourceVersioner 28 | } 29 | -------------------------------------------------------------------------------- /controllers/helpers/errors_test.go: -------------------------------------------------------------------------------- 1 | package helpers_test 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | . "github.com/RackHD/ipam/controllers/helpers" 8 | 9 | . "github.com/onsi/ginkgo" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("ErrorToHTTPStatus", func() { 14 | It("should return http.StatusNotFound if the error specifies 'not found'", func() { 15 | err := fmt.Errorf("not found") 16 | 17 | Expect(ErrorToHTTPStatus(err)).To(Equal(http.StatusNotFound)) 18 | }) 19 | 20 | It("should return http.StatusInternalServerError for all other errors", func() { 21 | err := fmt.Errorf("any other error") 22 | 23 | Expect(ErrorToHTTPStatus(err)).To(Equal(http.StatusInternalServerError)) 24 | }) 25 | }) 26 | 27 | var _ = Describe("HTTPStatusError", func() { 28 | It("should return the provided status via the HTTPStatus method", func() { 29 | err := NewHTTPStatusError(100, "hello") 30 | Expect(err.HTTPStatus()).To(Equal(100)) 31 | }) 32 | 33 | It("should return the provided message via the Error method", func() { 34 | err := NewHTTPStatusError(100, "hello") 35 | Expect(err.Error()).To(Equal("hello")) 36 | }) 37 | 38 | It("should format the provided message using fmt.Sprintf", func() { 39 | err := NewHTTPStatusError(100, "hello %s", "world") 40 | Expect(err.Error()).To(Equal("hello world")) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /resources/factory/factory.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | ) 8 | 9 | // factory is a storage map for resource creator functions registered via init. 10 | var factory = make(map[string]interfaces.ResourceCreator) 11 | 12 | // Register associates the resource identifier with a resource creator function. 13 | func Register(resource string, creator interfaces.ResourceCreator) { 14 | factory[resource] = creator 15 | } 16 | 17 | // Request finds a resource creator function by the resource identifier and calls 18 | // the creator returning the result. The resulting resource may be a default version 19 | // if the reqeusted version is not present. 20 | func Request(resource string, version string) (interfaces.Resource, error) { 21 | if creator, ok := factory[resource]; ok { 22 | return creator(version) 23 | } 24 | 25 | return nil, fmt.Errorf("Request: Unable to locate resource %s.", resource) 26 | } 27 | 28 | // Require finds a resource creator function by the resource identifier and verifies 29 | // the created resource matches the requested version. If not, an error will be 30 | // returned. 31 | func Require(resource string, version string) (interfaces.Resource, error) { 32 | provided, err := Request(resource, version) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | if provided.Version() != version { 38 | return nil, fmt.Errorf("Require: Unable to locate resource %s, version %s.", resource, version) 39 | } 40 | 41 | return provided, nil 42 | } 43 | -------------------------------------------------------------------------------- /controllers/helpers/media_type_test.go: -------------------------------------------------------------------------------- 1 | package helpers_test 2 | 3 | import ( 4 | . "github.com/RackHD/ipam/controllers/helpers" 5 | 6 | . "github.com/onsi/ginkgo" 7 | . "github.com/onsi/gomega" 8 | ) 9 | 10 | var _ = Describe("MediaType", func() { 11 | Describe("NewMediaType", func() { 12 | It("should return an error if parsing failed", func() { 13 | mt, err := NewMediaType("") 14 | 15 | Expect(err).To(HaveOccurred()) 16 | Expect(mt).To(BeZero()) 17 | }) 18 | 19 | It("should properly parse out an ipam resource", func() { 20 | mt, err := NewMediaType("application/vnd.ipam.pool") 21 | 22 | Expect(err).ToNot(HaveOccurred()) 23 | Expect(mt).To(Equal(MediaType{"application/vnd.ipam.pool", ""})) 24 | }) 25 | 26 | It("should properly parse out an ipam resource and format", func() { 27 | mt, err := NewMediaType("application/vnd.ipam.pool+xml") 28 | 29 | Expect(err).ToNot(HaveOccurred()) 30 | Expect(mt).To(Equal(MediaType{"application/vnd.ipam.pool", ""})) 31 | }) 32 | 33 | It("should propery parse out an ipam resource, format, and version", func() { 34 | mt, err := NewMediaType("application/vnd.ipam.pool+json;version=1.0.0") 35 | 36 | Expect(err).ToNot(HaveOccurred()) 37 | Expect(mt).To(Equal(MediaType{"application/vnd.ipam.pool", "1.0.0"})) 38 | }) 39 | 40 | It("should ignore any extra parameters", func() { 41 | mt, err := NewMediaType("application/vnd.ipam.pool+json;extra=lol;version=1.0.0") 42 | 43 | Expect(err).ToNot(HaveOccurred()) 44 | Expect(mt).To(Equal(MediaType{"application/vnd.ipam.pool", "1.0.0"})) 45 | }) 46 | }) 47 | }) 48 | -------------------------------------------------------------------------------- /ipam/leases.go: -------------------------------------------------------------------------------- 1 | package ipam 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | // IpamCollectionLeases is the name of the Mongo collection which stores Leases. 9 | const IpamCollectionLeases string = "leases" 10 | 11 | // GetLeases returns a list of Leases. 12 | func (ipam *Ipam) GetLeases(id string) ([]models.Lease, error) { 13 | session := ipam.session.Copy() 14 | defer session.Close() 15 | 16 | var reservations []models.Lease 17 | 18 | session.DB(IpamDatabase).C(IpamCollectionLeases).Find(bson.M{"reservation": bson.ObjectIdHex(id)}).All(&reservations) 19 | 20 | return reservations, nil 21 | } 22 | 23 | // GetLease returns the requested Lease. 24 | func (ipam *Ipam) GetLease(id string) (models.Lease, error) { 25 | session := ipam.session.Copy() 26 | defer session.Close() 27 | 28 | var reservation models.Lease 29 | 30 | return reservation, session.DB(IpamDatabase).C(IpamCollectionLeases).Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&reservation) 31 | } 32 | 33 | // UpdateLease updates a Lease. 34 | func (ipam *Ipam) UpdateLease(reservation models.Lease) error { 35 | session := ipam.session.Copy() 36 | defer session.Close() 37 | 38 | return session.DB(IpamDatabase).C(IpamCollectionLeases).UpdateId(reservation.ID, reservation) 39 | } 40 | 41 | // DeleteLeases removes all leases associated to a subnet 42 | func (ipam *Ipam) DeleteLeases(id string) error { 43 | session := ipam.session.Copy() 44 | defer session.Close() 45 | 46 | _, err := session.DB(IpamDatabase).C(IpamCollectionLeases).RemoveAll(bson.M{"subnet": bson.ObjectIdHex(id)}) 47 | return err 48 | } 49 | -------------------------------------------------------------------------------- /resources/factory/factory_test.go: -------------------------------------------------------------------------------- 1 | package factory_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/resources" 5 | . "github.com/RackHD/ipam/resources/factory" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Factory", func() { 12 | Describe("Request", func() { 13 | It("should return the requested resource", func() { 14 | resource, err := Request(resources.PoolResourceType, resources.PoolResourceVersionV1) 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&resources.PoolV1{})) 17 | }) 18 | 19 | It("should return an error if the requested resource is not registered", func() { 20 | _, err := Request("invalid", "1.0.0") 21 | Expect(err).To(HaveOccurred()) 22 | Expect(err.Error()).To(HavePrefix("Request")) 23 | }) 24 | }) 25 | 26 | Describe("Require", func() { 27 | It("should return the requested resource", func() { 28 | resource, err := Require(resources.PoolResourceType, resources.PoolResourceVersionV1) 29 | Expect(err).ToNot(HaveOccurred()) 30 | Expect(resource).To(BeAssignableToTypeOf(&resources.PoolV1{})) 31 | }) 32 | 33 | It("should return an error if the requested resource is not registered", func() { 34 | _, err := Require("invalid", "1.0.0") 35 | Expect(err).To(HaveOccurred()) 36 | Expect(err.Error()).To(HavePrefix("Request")) 37 | }) 38 | 39 | It("should return an error if the requested version is not registered", func() { 40 | _, err := Require(resources.PoolResourceType, "invalid") 41 | Expect(err).To(HaveOccurred()) 42 | Expect(err.Error()).To(HavePrefix("Require")) 43 | }) 44 | }) 45 | }) 46 | -------------------------------------------------------------------------------- /ipam/pools.go: -------------------------------------------------------------------------------- 1 | package ipam 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | // IpamCollectionPools is the name of the Mongo collection which stores Pools. 9 | const IpamCollectionPools string = "pools" 10 | 11 | // GetPools returns a list of Pools. 12 | func (ipam *Ipam) GetPools() ([]models.Pool, error) { 13 | session := ipam.session.Copy() 14 | defer session.Close() 15 | 16 | var pools []models.Pool 17 | 18 | session.DB(IpamDatabase).C(IpamCollectionPools).Find(nil).All(&pools) 19 | 20 | return pools, nil 21 | } 22 | 23 | // GetPool returns the requested Pool. 24 | func (ipam *Ipam) GetPool(id string) (models.Pool, error) { 25 | session := ipam.session.Copy() 26 | defer session.Close() 27 | 28 | var pool models.Pool 29 | 30 | return pool, session.DB(IpamDatabase).C(IpamCollectionPools).Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&pool) 31 | } 32 | 33 | // CreatePool creates a Pool. 34 | func (ipam *Ipam) CreatePool(pool models.Pool) error { 35 | session := ipam.session.Copy() 36 | defer session.Close() 37 | 38 | return session.DB(IpamDatabase).C(IpamCollectionPools).Insert(pool) 39 | } 40 | 41 | // UpdatePool updates a Pool. 42 | func (ipam *Ipam) UpdatePool(pool models.Pool) error { 43 | session := ipam.session.Copy() 44 | defer session.Close() 45 | 46 | return session.DB(IpamDatabase).C(IpamCollectionPools).UpdateId(pool.ID, pool) 47 | } 48 | 49 | // DeletePool removes a Pool. 50 | func (ipam *Ipam) DeletePool(id string) error { 51 | session := ipam.session.Copy() 52 | defer session.Close() 53 | 54 | err := ipam.DeleteSubnets(id) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return session.DB(IpamDatabase).C(IpamCollectionPools).RemoveId(bson.ObjectIdHex(id)) 60 | } 61 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APPLICATION = ipam 2 | ORGANIZATION = RackHD 3 | 4 | TTY = $(shell if [ -t 0 ]; then echo "-ti"; fi) 5 | 6 | DOCKER_DIR = /go/src/github.com/${ORGANIZATION}/${APPLICATION} 7 | DOCKER_IMAGE = rackhd/golang:1.7.1-wheezy 8 | DOCKER_CMD = docker run ${TTY} --rm -v ${PWD}:${DOCKER_DIR} -w ${DOCKER_DIR} ${DOCKER_IMAGE} 9 | 10 | .PHONY: shell deps deps-local build build-local lint lint-local test test-local release 11 | 12 | default: deps test build 13 | 14 | shell: 15 | @docker run --rm -ti -v ${PWD}:${DOCKER_DIR} -w ${DOCKER_DIR} ${DOCKER_IMAGE} /bin/bash 16 | 17 | deps: 18 | @${DOCKER_CMD} make deps-local 19 | 20 | deps-local: 21 | @if ! [ -f glide.yaml ]; then glide init --non-interactive; fi 22 | @glide install 23 | 24 | build: 25 | @${DOCKER_CMD} make build-local 26 | 27 | build-local: 28 | @go build -o bin/${APPLICATION} main.go 29 | 30 | lint: 31 | @${DOCKER_CMD} make lint-local 32 | 33 | lint-local: 34 | @gometalinter --vendor --fast --disable=dupl --disable=gotype --skip=grpc ./... 35 | 36 | test: 37 | @${DOCKER_CMD} make test-local 38 | 39 | test-client: 40 | @docker-compose build 41 | @docker-compose create 42 | @docker-compose start 43 | @-make test-client-local 44 | @docker-compose kill 45 | 46 | test-local: lint-local 47 | @ginkgo -race -trace -randomizeAllSpecs -r -cover --skipPackage=client 48 | 49 | test-client-local: 50 | @ginkgo -race -trace -randomizeAllSpecs -cover client 51 | 52 | coveralls: 53 | @go get github.com/mattn/goveralls 54 | @go get github.com/modocache/gover 55 | @go get golang.org/x/tools/cmd/cover 56 | @gover 57 | @goveralls -coverprofile=gover.coverprofile -service=travis-ci 58 | 59 | release: build 60 | @docker build -t rackhd/${APPLICATION} . 61 | 62 | run: release 63 | @docker-compose up 64 | 65 | mongo: 66 | @docker exec -it mongodb mongo ipam 67 | -------------------------------------------------------------------------------- /resources/pools.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | "github.com/RackHD/ipam/models" 8 | "github.com/RackHD/ipam/resources/factory" 9 | ) 10 | 11 | // PoolsResourceType is the media type assigned to a collection of Pool resources. 12 | const PoolsResourceType string = "application/vnd.ipam.pools" 13 | 14 | // PoolsResourceVersionV1 is the semantic version identifier for the Pool resource. 15 | const PoolsResourceVersionV1 string = "1.0.0" 16 | 17 | func init() { 18 | factory.Register(PoolsResourceType, PoolsCreator) 19 | } 20 | 21 | // PoolsCreator is a factory function for turning a version string into a Pools resource. 22 | func PoolsCreator(version string) (interfaces.Resource, error) { 23 | return &PoolsV1{}, nil 24 | } 25 | 26 | // PoolsV1 represents the v1.0.0 version of the Pools resource. 27 | type PoolsV1 struct { 28 | Pools []PoolV1 `json:"pools"` 29 | } 30 | 31 | // Type returns the resource type for use in rendering HTTP response headers. 32 | func (p *PoolsV1) Type() string { 33 | return PoolsResourceType 34 | } 35 | 36 | // Version returns the resource version for use in rendering HTTP response headers. 37 | func (p *PoolsV1) Version() string { 38 | return PoolsResourceVersionV1 39 | } 40 | 41 | // Marshal converts an array of models.Pool objects into this version of the resource. 42 | func (p *PoolsV1) Marshal(object interface{}) error { 43 | if pools, ok := object.([]models.Pool); ok { 44 | p.Pools = make([]PoolV1, len(pools)) 45 | 46 | for i := range p.Pools { 47 | p.Pools[i].Marshal(pools[i]) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | return fmt.Errorf("Invalid Object Type.") 54 | } 55 | 56 | // Unmarshal converts the resource into an array of models.Pool objects. 57 | func (p *PoolsV1) Unmarshal() (interface{}, error) { 58 | return nil, fmt.Errorf("Invalid Action for Resource.") 59 | } 60 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "log" 6 | "net/http" 7 | "os" 8 | 9 | "github.com/RackHD/ipam/controllers" 10 | "github.com/RackHD/ipam/ipam" 11 | "github.com/gorilla/handlers" 12 | "github.com/gorilla/mux" 13 | 14 | "gopkg.in/mgo.v2" 15 | ) 16 | 17 | var mongo string 18 | 19 | func init() { 20 | flag.StringVar(&mongo, "mongo", "ipam_mongo:27017", "port to connect to mongodb container") 21 | } 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | // Default to enable mgo debug. Set to false to disable. 27 | var mgoDebug = true 28 | 29 | if mgoDebug { 30 | mgo.SetDebug(true) 31 | var aLogger *log.Logger 32 | aLogger = log.New(os.Stderr, "", log.LstdFlags) 33 | mgo.SetLogger(aLogger) 34 | } 35 | 36 | // Start off with a new mux router. 37 | router := mux.NewRouter().StrictSlash(true) 38 | 39 | session, err := mgo.Dial(mongo) 40 | if err != nil { 41 | log.Fatalf("%s", err) 42 | } 43 | defer session.Close() 44 | 45 | // Create the IPAM business logic object. 46 | ipam, err := ipam.NewIpam(session) 47 | if err != nil { 48 | log.Fatalf("%s", err) 49 | } 50 | 51 | // Oddly enough don't need to capture the router for it to continue to exist. 52 | _, err = controllers.NewPoolsController(router, ipam) 53 | if err != nil { 54 | log.Fatalf("%s", err) 55 | } 56 | 57 | _, err = controllers.NewSubnetsController(router, ipam) 58 | if err != nil { 59 | log.Fatalf("%s", err) 60 | } 61 | 62 | _, err = controllers.NewReservationsController(router, ipam) 63 | if err != nil { 64 | log.Fatalf("%s", err) 65 | } 66 | 67 | _, err = controllers.NewLeasesController(router, ipam) 68 | if err != nil { 69 | log.Fatalf("%s", err) 70 | } 71 | 72 | // Show off request logging middleware. 73 | logged := handlers.LoggingHandler(os.Stdout, router) 74 | 75 | log.Printf("Listening on port 8000...") 76 | 77 | http.ListenAndServe(":8000", logged) 78 | } 79 | -------------------------------------------------------------------------------- /resources/leases.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | "github.com/RackHD/ipam/models" 8 | "github.com/RackHD/ipam/resources/factory" 9 | ) 10 | 11 | // LeasesResourceType is the media type assigned to a collection of Lease resources. 12 | const LeasesResourceType string = "application/vnd.ipam.leases" 13 | 14 | // LeasesResourceVersionV1 is the semantic version identifier for the Pool resource. 15 | const LeasesResourceVersionV1 string = "1.0.0" 16 | 17 | func init() { 18 | factory.Register(LeasesResourceType, LeasesCreator) 19 | } 20 | 21 | // LeasesCreator is a factory function for turning a version string into a Leases resource. 22 | func LeasesCreator(version string) (interfaces.Resource, error) { 23 | return &LeasesV1{}, nil 24 | } 25 | 26 | // LeasesV1 represents the v1.0.0 version of the Leases resource. 27 | type LeasesV1 struct { 28 | Leases []LeaseV1 `json:"leases"` 29 | } 30 | 31 | // Type returns the resource type for use in rendering HTTP response headers. 32 | func (p *LeasesV1) Type() string { 33 | return LeasesResourceType 34 | } 35 | 36 | // Version returns the resource version for use in rendering HTTP response headers. 37 | func (p *LeasesV1) Version() string { 38 | return LeasesResourceVersionV1 39 | } 40 | 41 | // Marshal converts an array of models.Lease objects into this version of the resource. 42 | func (p *LeasesV1) Marshal(object interface{}) error { 43 | if subnets, ok := object.([]models.Lease); ok { 44 | p.Leases = make([]LeaseV1, len(subnets)) 45 | 46 | for i := range p.Leases { 47 | p.Leases[i].Marshal(subnets[i]) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | return fmt.Errorf("Invalid Object Type.") 54 | } 55 | 56 | // Unmarshal converts the resource into an array of models.Lease objects. 57 | func (p *LeasesV1) Unmarshal() (interface{}, error) { 58 | return nil, fmt.Errorf("Invalid Action for Resource.") 59 | } 60 | -------------------------------------------------------------------------------- /resources/subnets.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | "github.com/RackHD/ipam/models" 8 | "github.com/RackHD/ipam/resources/factory" 9 | ) 10 | 11 | // SubnetsResourceType is the media type assigned to a collection of Subnet resources. 12 | const SubnetsResourceType string = "application/vnd.ipam.subnets" 13 | 14 | // SubnetsResourceVersionV1 is the semantic version identifier for the Pool resource. 15 | const SubnetsResourceVersionV1 string = "1.0.0" 16 | 17 | func init() { 18 | factory.Register(SubnetsResourceType, SubnetsCreator) 19 | } 20 | 21 | // SubnetsCreator is a factory function for turning a version string into a Subnets resource. 22 | func SubnetsCreator(version string) (interfaces.Resource, error) { 23 | return &SubnetsV1{}, nil 24 | } 25 | 26 | // SubnetsV1 represents the v1.0.0 version of the Subnets resource. 27 | type SubnetsV1 struct { 28 | Subnets []SubnetV1 `json:"subnets"` 29 | } 30 | 31 | // Type returns the resource type for use in rendering HTTP response headers. 32 | func (p *SubnetsV1) Type() string { 33 | return SubnetsResourceType 34 | } 35 | 36 | // Version returns the resource version for use in rendering HTTP response headers. 37 | func (p *SubnetsV1) Version() string { 38 | return SubnetsResourceVersionV1 39 | } 40 | 41 | // Marshal converts an array of models.Subnet objects into this version of the resource. 42 | func (p *SubnetsV1) Marshal(object interface{}) error { 43 | if subnets, ok := object.([]models.Subnet); ok { 44 | p.Subnets = make([]SubnetV1, len(subnets)) 45 | 46 | for i := range p.Subnets { 47 | p.Subnets[i].Marshal(subnets[i]) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | return fmt.Errorf("Invalid Object Type.") 54 | } 55 | 56 | // Unmarshal converts the resource into an array of models.Subnet objects. 57 | func (p *SubnetsV1) Unmarshal() (interface{}, error) { 58 | return nil, fmt.Errorf("Invalid Action for Resource.") 59 | } 60 | -------------------------------------------------------------------------------- /client/leases.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/RackHD/ipam/resources" 7 | ) 8 | 9 | // IndexLeases returns a list of Leases. 10 | func (c *Client) IndexLeases(reservationID string) (resources.LeasesV1, error) { 11 | returnedLeases, err := c.ReceiveResource("GET", "/reservations/"+reservationID+"/leases", "", "") 12 | if err != nil { 13 | return resources.LeasesV1{}, err 14 | } 15 | if leases, ok := returnedLeases.(*resources.LeasesV1); ok { 16 | return *leases, nil 17 | } 18 | return resources.LeasesV1{}, errors.New("Lease Index call error.") 19 | } 20 | 21 | // ShowLease returns the requested Lease. 22 | func (c *Client) ShowLease(leaseID string, leaseToShow resources.LeaseV1) (resources.LeaseV1, error) { 23 | returnedLease, err := c.ReceiveResource("GET", "/leases/"+leaseID, leaseToShow.Type(), leaseToShow.Version()) 24 | if err != nil { 25 | return resources.LeaseV1{}, err 26 | } 27 | if lease, ok := returnedLease.(*resources.LeaseV1); ok { 28 | return *lease, nil 29 | } 30 | return resources.LeaseV1{}, errors.New("Lease Show call error.") 31 | } 32 | 33 | // UpdateLease updates the requested Lease and returns its location. 34 | func (c *Client) UpdateLease(leaseID string, leaseToUpdate resources.LeaseV1) (string, error) { 35 | leaseLocation, err := c.SendResource("PATCH", "/leases/"+leaseID, &leaseToUpdate) 36 | if err != nil { 37 | return "", err 38 | } 39 | return leaseLocation, nil 40 | } 41 | 42 | // UpdateShowLease updates a Lease and then returns that Lease. 43 | func (c *Client) UpdateShowLease(leaseID string, leaseToUpdate resources.LeaseV1) (resources.LeaseV1, error) { 44 | returnedLease, err := c.SendReceiveResource("PATCH", "GET", "/leases/"+leaseID, &leaseToUpdate) 45 | if err != nil { 46 | return resources.LeaseV1{}, err 47 | } 48 | if lease, ok := returnedLease.(*resources.LeaseV1); ok { 49 | return *lease, nil 50 | } 51 | return resources.LeaseV1{}, errors.New("UpdateShowLease call error.") 52 | } 53 | -------------------------------------------------------------------------------- /controllers/helpers/errors.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | ) 7 | 8 | // ErrorHandler provides an error handling capable HTTP handler. 9 | type ErrorHandler func(http.ResponseWriter, *http.Request) error 10 | 11 | // ServeHTTP implements the standard HTTP handler interface and handles ErrorHandler 12 | // based handler functions. 13 | func (handler ErrorHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 14 | if err := handler(w, r); err != nil { 15 | RenderError(w, err) 16 | } 17 | } 18 | 19 | // StatusError provides an additional interface method on errors to query an 20 | // error for a HTTP status code. 21 | type StatusError interface { 22 | error 23 | HTTPStatus() int 24 | } 25 | 26 | // HTTPStatusError implements the StatusError interface to provide HTTP status 27 | // aware errors. 28 | type HTTPStatusError struct { 29 | status int 30 | message string 31 | } 32 | 33 | // NewHTTPStatusError returns a properly configured HTTPStatusError object. 34 | func NewHTTPStatusError(status int, format string, a ...interface{}) HTTPStatusError { 35 | return HTTPStatusError{ 36 | status: status, 37 | message: fmt.Sprintf(format, a...), 38 | } 39 | } 40 | 41 | // Error is the implementation of the standard library error interface. 42 | func (err HTTPStatusError) Error() string { 43 | return err.message 44 | } 45 | 46 | // HTTPStatus returns the configured status code for use in HTTP responses. 47 | func (err HTTPStatusError) HTTPStatus() int { 48 | return err.status 49 | } 50 | 51 | // ErrorToHTTPStatus converts an error into a proper HTTP status code for use in our routers. 52 | // It is a fallback for errors produced in libraries we don't control which utilize error 53 | // strings to denote their type of behavior. Our primary case at this point is the mgo 'not found' error. 54 | func ErrorToHTTPStatus(err error) int { 55 | switch err.Error() { 56 | case "not found": 57 | return http.StatusNotFound 58 | default: 59 | return http.StatusInternalServerError 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/RackHD/ipam.svg?branch=master)](https://travis-ci.org/RackHD/ipam) 2 | [![Coverage Status](https://coveralls.io/repos/github/RackHD/ipam/badge.svg?branch=master)](https://coveralls.io/github/RackHD/ipam?branch=master) 3 | [![GoDoc](https://godoc.org/github.com/RackHD/ipam?status.svg)](https://godoc.org/github.com/RackHD/ipam) 4 | 5 | Copyright © 2017 Dell Inc. or its subsidiaries. All Rights Reserved. 6 | 7 | Contribute 8 | ---------- 9 | 10 | IPAM is a collection of libraries and applications housed at https://github.com/RackHD/ipam. The code for IPAM is written in Golang and makes use of Makefiles. It is available under the Apache 2.0 license (or compatible sublicences for library dependencies). 11 | 12 | Code and bug submissions are handled on GitHub using the Issues tab for this repository above. 13 | 14 | Community 15 | --------- 16 | 17 | We also have a #InfraEnablers Slack channel: You can get an invite by requesting one at http://community.emccode.com. 18 | 19 | Documentation 20 | ------------- 21 | 22 | You can find documentation for IPAM here: [![GoDoc](https://godoc.org/github.com/RackHD/ipam?status.svg)](https://godoc.org/github.com/RackHD/ipam) 23 | 24 | Licensing 25 | --------- 26 | 27 | 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 28 | 29 | 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. 30 | 31 | RackHD is a Trademark of EMC Corporation 32 | 33 | Support 34 | ------- 35 | 36 | Please file bugs and issues at the GitHub issues page. The code and documentation are released with no warranties or SLAs and are intended to be supported through a community driven process. 37 | -------------------------------------------------------------------------------- /resources/pools_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | . "github.com/RackHD/ipam/resources" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("PoolsCreator", func() { 13 | It("should return a PoolsV1 resource by default", func() { 14 | resource, err := PoolsCreator("") 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&PoolsV1{})) 17 | }) 18 | }) 19 | 20 | var _ = Describe("PoolsV1", func() { 21 | var ( 22 | resource = PoolsV1{} 23 | pools = []models.Pool{ 24 | { 25 | ID: bson.NewObjectId(), 26 | Name: "PoolV1 Name", 27 | Tags: []string{"PoolV1"}, 28 | Metadata: "PoolV1 Metadata", 29 | }, 30 | } 31 | ) 32 | 33 | Describe("Type", func() { 34 | It("should return the correct resource type", func() { 35 | Expect(resource.Type()).To(Equal(PoolsResourceType)) 36 | }) 37 | }) 38 | 39 | Describe("Version", func() { 40 | It("should return the correct resource version", func() { 41 | Expect(resource.Version()).To(Equal(PoolsResourceVersionV1)) 42 | }) 43 | }) 44 | 45 | Describe("Marshal", func() { 46 | It("should copy the []models.Pool to itself", func() { 47 | err := resource.Marshal(pools) 48 | Expect(err).ToNot(HaveOccurred()) 49 | 50 | Expect(len(resource.Pools)).To(Equal(1)) 51 | 52 | Expect(resource.Pools[0].ID).To(Equal(pools[0].ID.Hex())) 53 | Expect(resource.Pools[0].Name).To(Equal(pools[0].Name)) 54 | Expect(resource.Pools[0].Tags).To(Equal(pools[0].Tags)) 55 | Expect(resource.Pools[0].Metadata).To(Equal(pools[0].Metadata)) 56 | }) 57 | 58 | It("should return an error if a []model.Pool is not provided", func() { 59 | err := resource.Marshal("invalid") 60 | Expect(err).To(HaveOccurred()) 61 | }) 62 | }) 63 | 64 | Describe("Unmarshal", func() { 65 | It("should return an error because the operation is not supported", func() { 66 | _, err := resource.Unmarshal() 67 | Expect(err).To(HaveOccurred()) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /resources/reservations.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | "github.com/RackHD/ipam/models" 8 | "github.com/RackHD/ipam/resources/factory" 9 | ) 10 | 11 | // ReservationsResourceType is the media type assigned to a collection of Reservation resources. 12 | const ReservationsResourceType string = "application/vnd.ipam.reservations" 13 | 14 | // ReservationsResourceVersionV1 is the semantic version identifier for the Pool resource. 15 | const ReservationsResourceVersionV1 string = "1.0.0" 16 | 17 | func init() { 18 | factory.Register(ReservationsResourceType, ReservationsCreator) 19 | } 20 | 21 | // ReservationsCreator is a factory function for turning a version string into a Reservations resource. 22 | func ReservationsCreator(version string) (interfaces.Resource, error) { 23 | return &ReservationsV1{}, nil 24 | } 25 | 26 | // ReservationsV1 represents the v1.0.0 version of the Reservations resource. 27 | type ReservationsV1 struct { 28 | Reservations []ReservationV1 `json:"reservations"` 29 | } 30 | 31 | // Type returns the resource type for use in rendering HTTP response headers. 32 | func (p *ReservationsV1) Type() string { 33 | return ReservationsResourceType 34 | } 35 | 36 | // Version returns the resource version for use in rendering HTTP response headers. 37 | func (p *ReservationsV1) Version() string { 38 | return ReservationsResourceVersionV1 39 | } 40 | 41 | // Marshal converts an array of models.Reservation objects into this version of the resource. 42 | func (p *ReservationsV1) Marshal(object interface{}) error { 43 | if reservations, ok := object.([]models.Reservation); ok { 44 | p.Reservations = make([]ReservationV1, len(reservations)) 45 | 46 | for i := range p.Reservations { 47 | p.Reservations[i].Marshal(reservations[i]) 48 | } 49 | 50 | return nil 51 | } 52 | 53 | return fmt.Errorf("Invalid Object Type.") 54 | } 55 | 56 | // Unmarshal converts the resource into an array of models.Reservation objects. 57 | func (p *ReservationsV1) Unmarshal() (interface{}, error) { 58 | return nil, fmt.Errorf("Invalid Action for Resource.") 59 | } 60 | -------------------------------------------------------------------------------- /resources/subnets_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | . "github.com/RackHD/ipam/resources" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("SubnetsCreator", func() { 13 | It("should return a SubnetsV1 resource by default", func() { 14 | resource, err := SubnetsCreator("") 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&SubnetsV1{})) 17 | }) 18 | }) 19 | 20 | var _ = Describe("SubnetsV1", func() { 21 | var ( 22 | resource = SubnetsV1{} 23 | subnets = []models.Subnet{ 24 | { 25 | ID: bson.NewObjectId(), 26 | Name: "SubnetV1 Name", 27 | Tags: []string{"SubnetV1"}, 28 | Metadata: "SubnetV1 Metadata", 29 | }, 30 | } 31 | ) 32 | 33 | Describe("Type", func() { 34 | It("should return the correct resource type", func() { 35 | Expect(resource.Type()).To(Equal(SubnetsResourceType)) 36 | }) 37 | }) 38 | 39 | Describe("Version", func() { 40 | It("should return the correct resource version", func() { 41 | Expect(resource.Version()).To(Equal(SubnetsResourceVersionV1)) 42 | }) 43 | }) 44 | 45 | Describe("Marshal", func() { 46 | It("should copy the []models.Subnet to itself", func() { 47 | err := resource.Marshal(subnets) 48 | Expect(err).ToNot(HaveOccurred()) 49 | 50 | Expect(len(resource.Subnets)).To(Equal(1)) 51 | 52 | Expect(resource.Subnets[0].ID).To(Equal(subnets[0].ID.Hex())) 53 | Expect(resource.Subnets[0].Name).To(Equal(subnets[0].Name)) 54 | Expect(resource.Subnets[0].Tags).To(Equal(subnets[0].Tags)) 55 | Expect(resource.Subnets[0].Metadata).To(Equal(subnets[0].Metadata)) 56 | }) 57 | 58 | It("should return an error if a []model.Subnet is not provided", func() { 59 | err := resource.Marshal("invalid") 60 | Expect(err).To(HaveOccurred()) 61 | }) 62 | }) 63 | 64 | Describe("Unmarshal", func() { 65 | It("should return an error because the operation is not supported", func() { 66 | _, err := resource.Unmarshal() 67 | Expect(err).To(HaveOccurred()) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /resources/pool.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | "github.com/RackHD/ipam/models" 8 | "github.com/RackHD/ipam/resources/factory" 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | // PoolResourceType is the media type assigned to a Pool resource. 13 | const PoolResourceType string = "application/vnd.ipam.pool" 14 | 15 | // PoolResourceVersionV1 is the semantic version identifier for the Pool resource. 16 | const PoolResourceVersionV1 string = "1.0.0" 17 | 18 | func init() { 19 | factory.Register(PoolResourceType, PoolCreator) 20 | } 21 | 22 | // PoolCreator is a factory function for turning a version string into a Pool resource. 23 | func PoolCreator(version string) (interfaces.Resource, error) { 24 | return &PoolV1{}, nil 25 | } 26 | 27 | // PoolV1 represents the v1.0.0 version of the Pool resource. 28 | type PoolV1 struct { 29 | ID string `json:"id"` 30 | Name string `json:"name"` 31 | Tags []string `json:"tags"` 32 | Metadata interface{} `json:"metadata"` 33 | } 34 | 35 | // Type returns the resource type for use in rendering HTTP response headers. 36 | func (p *PoolV1) Type() string { 37 | return PoolResourceType 38 | } 39 | 40 | // Version returns the resource version for use in rendering HTTP response headers. 41 | func (p *PoolV1) Version() string { 42 | return PoolResourceVersionV1 43 | } 44 | 45 | // Marshal converts a models.Pool object into this version of the resource. 46 | func (p *PoolV1) Marshal(object interface{}) error { 47 | if target, ok := object.(models.Pool); ok { 48 | p.ID = target.ID.Hex() 49 | p.Name = target.Name 50 | p.Tags = target.Tags 51 | p.Metadata = target.Metadata 52 | 53 | return nil 54 | } 55 | 56 | return fmt.Errorf("Invalid Object Type: %+v", object) 57 | } 58 | 59 | // Unmarshal converts the resource into a models.Pool object. 60 | func (p *PoolV1) Unmarshal() (interface{}, error) { 61 | if p.ID == "" { 62 | p.ID = bson.NewObjectId().Hex() 63 | } 64 | 65 | return models.Pool{ 66 | ID: bson.ObjectIdHex(p.ID), 67 | Name: p.Name, 68 | Tags: p.Tags, 69 | Metadata: p.Metadata, 70 | }, nil 71 | } 72 | -------------------------------------------------------------------------------- /resources/reservations_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | . "github.com/RackHD/ipam/resources" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("ReservationsCreator", func() { 13 | It("should return a ReservationsV1 resource by default", func() { 14 | resource, err := ReservationsCreator("") 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&ReservationsV1{})) 17 | }) 18 | }) 19 | 20 | var _ = Describe("ReservationsV1", func() { 21 | var ( 22 | resource = ReservationsV1{} 23 | reservations = []models.Reservation{ 24 | { 25 | ID: bson.NewObjectId(), 26 | Name: "ReservationV1 Name", 27 | Tags: []string{"ReservationV1"}, 28 | Metadata: "ReservationV1 Metadata", 29 | }, 30 | } 31 | ) 32 | 33 | Describe("Type", func() { 34 | It("should return the correct resource type", func() { 35 | Expect(resource.Type()).To(Equal(ReservationsResourceType)) 36 | }) 37 | }) 38 | 39 | Describe("Version", func() { 40 | It("should return the correct resource version", func() { 41 | Expect(resource.Version()).To(Equal(ReservationsResourceVersionV1)) 42 | }) 43 | }) 44 | 45 | Describe("Marshal", func() { 46 | It("should copy the []models.Reservation to itself", func() { 47 | err := resource.Marshal(reservations) 48 | Expect(err).ToNot(HaveOccurred()) 49 | 50 | Expect(len(resource.Reservations)).To(Equal(1)) 51 | 52 | Expect(resource.Reservations[0].ID).To(Equal(reservations[0].ID.Hex())) 53 | Expect(resource.Reservations[0].Name).To(Equal(reservations[0].Name)) 54 | Expect(resource.Reservations[0].Tags).To(Equal(reservations[0].Tags)) 55 | Expect(resource.Reservations[0].Metadata).To(Equal(reservations[0].Metadata)) 56 | }) 57 | 58 | It("should return an error if a []model.Reservation is not provided", func() { 59 | err := resource.Marshal("invalid") 60 | Expect(err).To(HaveOccurred()) 61 | }) 62 | }) 63 | 64 | Describe("Unmarshal", func() { 65 | It("should return an error because the operation is not supported", func() { 66 | _, err := resource.Unmarshal() 67 | Expect(err).To(HaveOccurred()) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: required 2 | services: 3 | - docker 4 | language: go 5 | go: 6 | - 1.7 7 | before_install: 8 | - echo -e "machine github.com\n login $GITHUB_USER\n password $GITHUB_PASS" >> ~/.netrc 9 | script: 10 | - make 11 | after_success: 12 | - make coveralls 13 | env: 14 | global: 15 | - secure: udHxDWDOP6688+nxPyblKMPptstQkC7TYjEfODOQArUPov5uuRmNtJTG1eDhMfGTCKwSrPigug3HxrvZtiVrPl/nwfKAp/Sa7jKnuxOB+3IFUnxQyCxjtLs6iaNRHblwD9yY7Dyr7URAep19nYJ8sinZ/nEMm40wcWJwrsAT/4/w3Imxv3VNzhWOIwon/jl4Hl9rZzugCUwAQwMRq5saZhap5/0Uhzb4jKL5tp9r2S2HeDcWm5GQwFCH/kAx917tIqdQXTSqrduWnraxYi0hbb9Z2sIKDYhNjMuMBYXFvLEyRoByvlwMuf/LZ7MbfzHvScw8DKqkLbz48iQmnlRNWpYaFlgYKYNy0P96DBVVpFY3GBc/NNCrrmTi/8M+HwG4Ae3euhL4eHMHwh7P6o0IZPidYog1KDpU7co9NoDc3pLU+l0ZBM8AKkfudOjtUG2PZn9pRApLJxTJv6BC4c/b7TZtNlzJ2/RSdsbd312tymWgvcpYguYvClVy2xz5fk9AXoCH6ho37sGuKnPsNd2cw+AzQnAk1H9iJAUo57lPtEu0AO2IHXYqK4hWABxqolZHNoOVnKKra6uEqoL5wRaEK1soxLzSm78MJaPea2C8hdN5McHSOd8zNzc4iljL/B9pgpEBgFeru8AQDFIXzrI0DMYhuuXTXwyjpbwh9cicdr0= 16 | - secure: kyCYGqopjejN0DYMFGNOmOq9uHuGV/ssi2dti/6KC3pWdwCuS7af2kiUqR5WWaN0yGhvr66cN7UcAZLMEg/71sMMiFBa0Dp8H5Hl8CL5/UeitAINfX3spRsq4xpo3u5bRweU7s2/8I1wTtq+hXRRVNO/rVB2mN6reVPsEzSZnexZxa60EHhdA1P4Gbg8MNmNJijEuR8NZpx40ngd74CK5CDGgaid7AtEtYhlC9jp7zOMGxZjCniTob7qvnXT5Bf/i+8N7wZDl/JqT6PkGVNiJTB+TCFQEePGXhYds3biehCmIGqrCpFAahm5MVay/R5Q8HNIU0jyuTjkBv1RJGhYZ+DSm4+C3+4Tfo04lANAA/ZTeI8Nnb29fJyXoeAYfmD2oyEsftcoDYt+2L/2/wtSvmtXCZhrSalLRwAhLhRNIEpDlQtJEbh2AYjVZU6NZQks+7vXWQy+2x6mf3haAG/aPd2NX0308orMx42RV6so5XeYNEzIt34glfWiKATVqB/l6wgyJ5kZi1zHwDstByDJaXFjnPxUr406EEPZR3R9ypqcjPaZbb+xui5xsBLZ0nxmyHPytVM/6nqNv3q/r0iipO7NmdWnJk7u9dtxEt+jDkxMm8ccbXxKdaxSJVv+7HEbRP5yHckvBzRmhtTMi5hJFDq6yEcflpgQwKjaMY/2dxo= 17 | notifications: 18 | slack: 19 | secure: uFsPAXCnFrBfAYDTaNVDcRMmpV++qJ4VgnZhYhw/A5e8V7kGDIrsXd1fa9FCjOHmlaTIKkBQDFucp7oxdpBRhOpvuE/IFcXNCUkklojvQEtknFSDiNysxxF5QV98ltXBsK/1smVHBeJbfwQ3nmitXObVafz3cDITK3ZzBVCGdmH4RHV2kj92+j7cYPUPjF9u7yswXJS6iSF0JqWBEe1rCAKa4setfxRguPH/G39fjlpdJlHNwsuiOLDY9i2vg+LUKlioaESrppkwljNdZIVdDAswyiCHyhHog/i6vhmw8m2vbBUo1Dek45OVVnUdKqltg/Ded5GpZ2VDpJkGenUSsVLwV3krKQeT2OTWzHuBeuLZY9XJAbOjOgh6YQ5cEeHsFlfbaylqPkd3W6gHV/1CvhxzXmsqsDOwyfIAJqr8vSwKTd6y/tBf1gN6bdg00SSuzUDjpe5SfoYx2KEizIT/e+Ne4t9iPVRG16Mn1hDJC/arBOCBI9o2LYYaDY9cnvRTM6TYUU1WetXdqvUtNqnf4uZccBB109YGOhCv3wqQNnLe64geKlr+9T0+PWc5Q2xTjACLWmg3/8A4XgoWq3h7PA6MAjeNoJWLx3C/X43T+iBAJPL0DN2TMl8IYzs8Y+Wc9zuh5dEaZoBgmvnWw5+6yOkNnLBDzt4X/+j2/nTxndM= 20 | -------------------------------------------------------------------------------- /resources/reservation.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/RackHD/ipam/interfaces" 7 | "github.com/RackHD/ipam/models" 8 | "github.com/RackHD/ipam/resources/factory" 9 | "gopkg.in/mgo.v2/bson" 10 | ) 11 | 12 | // ReservationResourceType is the media type assigned to a Reservation resource. 13 | const ReservationResourceType string = "application/vnd.ipam.reservation" 14 | 15 | // ReservationResourceVersionV1 is the semantic version identifier for the Subnet resource. 16 | const ReservationResourceVersionV1 string = "1.0.0" 17 | 18 | func init() { 19 | factory.Register(ReservationResourceType, ReservationCreator) 20 | } 21 | 22 | // ReservationCreator is a factory function for turning a version string into a Reservation resource. 23 | func ReservationCreator(version string) (interfaces.Resource, error) { 24 | return &ReservationV1{}, nil 25 | } 26 | 27 | // ReservationV1 represents the v1.0.0 version of the Reservation resource. 28 | type ReservationV1 struct { 29 | ID string `json:"id"` 30 | Name string `json:"name"` 31 | Tags []string `json:"tags"` 32 | Metadata interface{} `json:"metadata"` 33 | Subnet string `json:"subnet"` //Subnet ID 34 | } 35 | 36 | // Type returns the resource type for use in rendering HTTP response headers. 37 | func (s *ReservationV1) Type() string { 38 | return ReservationResourceType 39 | } 40 | 41 | // Version returns the resource version for use in rendering HTTP response headers. 42 | func (s *ReservationV1) Version() string { 43 | return ReservationResourceVersionV1 44 | } 45 | 46 | // Marshal converts a models.Reservation object into this version of the resource. 47 | func (s *ReservationV1) Marshal(object interface{}) error { 48 | if target, ok := object.(models.Reservation); ok { 49 | s.ID = target.ID.Hex() 50 | s.Name = target.Name 51 | s.Tags = target.Tags 52 | s.Metadata = target.Metadata 53 | s.Subnet = target.Subnet.Hex() 54 | 55 | return nil 56 | } 57 | 58 | return fmt.Errorf("Invalid Object Type: %+v", object) 59 | } 60 | 61 | // Unmarshal converts the resource into a models.Reservation object. 62 | func (s *ReservationV1) Unmarshal() (interface{}, error) { 63 | if s.ID == "" { 64 | s.ID = bson.NewObjectId().Hex() 65 | } 66 | 67 | return models.Reservation{ 68 | ID: bson.ObjectIdHex(s.ID), 69 | Name: s.Name, 70 | Tags: s.Tags, 71 | Metadata: s.Metadata, 72 | }, nil 73 | } 74 | -------------------------------------------------------------------------------- /controllers/leases.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "gopkg.in/mgo.v2/bson" 8 | 9 | "github.com/RackHD/ipam/controllers/helpers" 10 | "github.com/RackHD/ipam/interfaces" 11 | "github.com/RackHD/ipam/models" 12 | "github.com/RackHD/ipam/resources" 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | // LeasesController provides methods for handling requests to the Leases API. 17 | type LeasesController struct { 18 | ipam interfaces.Ipam 19 | } 20 | 21 | // NewLeasesController returns a newly configured LeasesController. 22 | func NewLeasesController(router *mux.Router, ipam interfaces.Ipam) (*LeasesController, error) { 23 | c := LeasesController{ 24 | ipam: ipam, 25 | } 26 | 27 | router.Handle("/reservations/{id}/leases", helpers.ErrorHandler(c.Index)).Methods(http.MethodGet) 28 | router.Handle("/leases/{id}", helpers.ErrorHandler(c.Show)).Methods(http.MethodGet) 29 | router.Handle("/leases/{id}", helpers.ErrorHandler(c.Update)).Methods(http.MethodPut, http.MethodPatch) 30 | 31 | return &c, nil 32 | } 33 | 34 | // Index returns a list of Leases. 35 | func (c *LeasesController) Index(w http.ResponseWriter, r *http.Request) error { 36 | vars := mux.Vars(r) 37 | 38 | reservations, err := c.ipam.GetLeases(vars["id"]) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return helpers.RenderResource(w, r, resources.LeasesResourceType, http.StatusOK, reservations) 44 | } 45 | 46 | // Show returns the requested Lease. 47 | func (c *LeasesController) Show(w http.ResponseWriter, r *http.Request) error { 48 | vars := mux.Vars(r) 49 | 50 | reservation, err := c.ipam.GetLease(vars["id"]) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return helpers.RenderResource(w, r, resources.LeaseResourceType, http.StatusOK, reservation) 56 | } 57 | 58 | // Update updates the requested Lease. 59 | func (c *LeasesController) Update(w http.ResponseWriter, r *http.Request) error { 60 | vars := mux.Vars(r) 61 | 62 | resource, err := helpers.AcceptResource(r, resources.LeaseResourceType) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if reservation, ok := resource.(models.Lease); ok { 68 | reservation.ID = bson.ObjectIdHex(vars["id"]) 69 | 70 | err = c.ipam.UpdateLease(reservation) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | return helpers.RenderLocation(w, http.StatusNoContent, fmt.Sprintf("/reservations/%s", reservation.ID.Hex())) 76 | } 77 | 78 | return fmt.Errorf("Invalid Resource Type") 79 | } 80 | -------------------------------------------------------------------------------- /resources/lease.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/RackHD/ipam/interfaces" 8 | "github.com/RackHD/ipam/models" 9 | "github.com/RackHD/ipam/resources/factory" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | // LeaseResourceType is the media type assigned to a Lease resource. 14 | const LeaseResourceType string = "application/vnd.ipam.lease" 15 | 16 | // LeaseResourceVersionV1 is the semantic version identifier for the Subnet resource. 17 | const LeaseResourceVersionV1 string = "1.0.0" 18 | 19 | func init() { 20 | factory.Register(LeaseResourceType, LeaseCreator) 21 | } 22 | 23 | // LeaseCreator is a factory function for turning a version string into a Lease resource. 24 | func LeaseCreator(version string) (interfaces.Resource, error) { 25 | return &LeaseV1{}, nil 26 | } 27 | 28 | // LeaseV1 represents the v1.0.0 version of the Lease resource. 29 | type LeaseV1 struct { 30 | ID string `json:"id"` 31 | Name string `json:"name"` 32 | Tags []string `json:"tags"` 33 | Metadata interface{} `json:"metadata"` 34 | Subnet string `json:"subnet"` //SubnetID 35 | Reservation string `json:"reservation"` //ReservationID 36 | Address string `json:"address"` 37 | } 38 | 39 | // Type returns the resource type for use in rendering HTTP response headers. 40 | func (s *LeaseV1) Type() string { 41 | return LeaseResourceType 42 | } 43 | 44 | // Version returns the resource version for use in rendering HTTP response headers. 45 | func (s *LeaseV1) Version() string { 46 | return LeaseResourceVersionV1 47 | } 48 | 49 | // Marshal converts a models.Lease object into this version of the resource. 50 | func (s *LeaseV1) Marshal(object interface{}) error { 51 | if target, ok := object.(models.Lease); ok { 52 | s.ID = target.ID.Hex() 53 | s.Name = target.Name 54 | s.Tags = target.Tags 55 | s.Metadata = target.Metadata 56 | s.Subnet = target.Subnet.Hex() 57 | s.Reservation = target.Reservation.Hex() 58 | s.Address = net.IP(target.Address.Data).String() 59 | 60 | return nil 61 | } 62 | 63 | return fmt.Errorf("Invalid Object Type: %+v", object) 64 | } 65 | 66 | // Unmarshal converts the resource into a models.Lease object. 67 | func (s *LeaseV1) Unmarshal() (interface{}, error) { 68 | if s.ID == "" { 69 | s.ID = bson.NewObjectId().Hex() 70 | } 71 | 72 | return models.Lease{ 73 | ID: bson.ObjectIdHex(s.ID), 74 | Name: s.Name, 75 | Tags: s.Tags, 76 | Metadata: s.Metadata, 77 | }, nil 78 | } 79 | -------------------------------------------------------------------------------- /resources/subnet.go: -------------------------------------------------------------------------------- 1 | package resources 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | 7 | "github.com/RackHD/ipam/interfaces" 8 | "github.com/RackHD/ipam/models" 9 | "github.com/RackHD/ipam/resources/factory" 10 | "gopkg.in/mgo.v2/bson" 11 | ) 12 | 13 | // SubnetResourceType is the media type assigned to a Subnet resource. 14 | const SubnetResourceType string = "application/vnd.ipam.subnet" 15 | 16 | // SubnetResourceVersionV1 is the semantic version identifier for the Pool resource. 17 | const SubnetResourceVersionV1 string = "1.0.0" 18 | 19 | func init() { 20 | factory.Register(SubnetResourceType, SubnetCreator) 21 | } 22 | 23 | // SubnetCreator is a factory function for turning a version string into a Subnet resource. 24 | func SubnetCreator(version string) (interfaces.Resource, error) { 25 | return &SubnetV1{}, nil 26 | } 27 | 28 | // SubnetV1 represents the v1.0.0 version of the Subnet resource. 29 | type SubnetV1 struct { 30 | ID string `json:"id"` 31 | Name string `json:"name"` 32 | Tags []string `json:"tags"` 33 | Metadata interface{} `json:"metadata"` 34 | Pool string `json:"pool"` //PoolID 35 | Start string `json:"start"` 36 | End string `json:"end"` 37 | } 38 | 39 | // Type returns the resource type for use in rendering HTTP response headers. 40 | func (s *SubnetV1) Type() string { 41 | return SubnetResourceType 42 | } 43 | 44 | // Version returns the resource version for use in rendering HTTP response headers. 45 | func (s *SubnetV1) Version() string { 46 | return SubnetResourceVersionV1 47 | } 48 | 49 | // Marshal converts a models.Subnet object into this version of the resource. 50 | func (s *SubnetV1) Marshal(object interface{}) error { 51 | if target, ok := object.(models.Subnet); ok { 52 | s.ID = target.ID.Hex() 53 | s.Name = target.Name 54 | s.Tags = target.Tags 55 | s.Metadata = target.Metadata 56 | s.Pool = target.Pool.Hex() 57 | s.Start = net.IP(target.Start.Data).String() 58 | s.End = net.IP(target.End.Data).String() 59 | 60 | return nil 61 | } 62 | 63 | return fmt.Errorf("Invalid Object Type: %+v", object) 64 | } 65 | 66 | // Unmarshal converts the resource into a models.Subnet object. 67 | func (s *SubnetV1) Unmarshal() (interface{}, error) { 68 | if s.ID == "" { 69 | s.ID = bson.NewObjectId().Hex() 70 | } 71 | 72 | return models.Subnet{ 73 | ID: bson.ObjectIdHex(s.ID), 74 | Name: s.Name, 75 | Tags: s.Tags, 76 | Metadata: s.Metadata, 77 | Start: bson.Binary{ 78 | Kind: 0, 79 | Data: net.ParseIP(s.Start), 80 | }, 81 | End: bson.Binary{ 82 | Kind: 0, 83 | Data: net.ParseIP(s.End), 84 | }, 85 | }, nil 86 | } 87 | -------------------------------------------------------------------------------- /resources/pool_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | . "github.com/RackHD/ipam/resources" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("PoolCreator", func() { 13 | It("should return a PoolV1 resource by default", func() { 14 | resource, err := PoolCreator("") 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&PoolV1{})) 17 | }) 18 | }) 19 | 20 | var _ = Describe("PoolV1", func() { 21 | var ( 22 | resource = PoolV1{ 23 | ID: bson.NewObjectId().Hex(), 24 | Name: "PoolV1 Name", 25 | Tags: []string{"PoolV1"}, 26 | Metadata: "PoolV1 Metadata", 27 | } 28 | model = models.Pool{ 29 | ID: bson.NewObjectId(), 30 | Name: "Pool Name", 31 | Tags: []string{"Pool"}, 32 | Metadata: "Pool Metadata", 33 | } 34 | ) 35 | 36 | Describe("Type", func() { 37 | It("should return the correct resource type", func() { 38 | Expect(resource.Type()).To(Equal(PoolResourceType)) 39 | }) 40 | }) 41 | 42 | Describe("Version", func() { 43 | It("should return the correct resource version", func() { 44 | Expect(resource.Version()).To(Equal(PoolResourceVersionV1)) 45 | }) 46 | }) 47 | 48 | Describe("Marshal", func() { 49 | It("should copy the models.Pool to itself", func() { 50 | err := resource.Marshal(model) 51 | Expect(err).ToNot(HaveOccurred()) 52 | 53 | Expect(resource.ID).To(Equal(model.ID.Hex())) 54 | Expect(resource.Name).To(Equal(model.Name)) 55 | Expect(resource.Tags).To(Equal(model.Tags)) 56 | Expect(resource.Metadata).To(Equal(model.Metadata)) 57 | }) 58 | 59 | It("should return an error if a model.Pool is not provided", func() { 60 | err := resource.Marshal("invalid") 61 | Expect(err).To(HaveOccurred()) 62 | }) 63 | }) 64 | 65 | Describe("Unmarshal", func() { 66 | It("should copy itself to a models.Pool", func() { 67 | m, err := resource.Unmarshal() 68 | Expect(err).ToNot(HaveOccurred()) 69 | Expect(m).To(BeAssignableToTypeOf(models.Pool{})) 70 | 71 | if result, ok := m.(models.Pool); ok { 72 | Expect(result.ID.Hex()).To(Equal(resource.ID)) 73 | Expect(result.Name).To(Equal(resource.Name)) 74 | Expect(result.Tags).To(Equal(resource.Tags)) 75 | Expect(result.Metadata).To(Equal(resource.Metadata)) 76 | } 77 | }) 78 | 79 | It("should generate a new object ID if one is not present", func() { 80 | resource.ID = "" 81 | 82 | m, err := resource.Unmarshal() 83 | Expect(err).ToNot(HaveOccurred()) 84 | Expect(m).To(BeAssignableToTypeOf(models.Pool{})) 85 | 86 | if result, ok := m.(models.Pool); ok { 87 | Expect(result.ID.Hex()).To(Equal(resource.ID)) 88 | Expect(result.Name).To(Equal(resource.Name)) 89 | Expect(result.Tags).To(Equal(resource.Tags)) 90 | Expect(result.Metadata).To(Equal(resource.Metadata)) 91 | } 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /client/pools.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/RackHD/ipam/resources" 7 | ) 8 | 9 | // IndexPools returns a list of Pools. 10 | func (c *Client) IndexPools() (resources.PoolsV1, error) { 11 | pools, err := c.ReceiveResource("GET", "/pools", "", "") 12 | if err != nil { 13 | return resources.PoolsV1{}, err 14 | } 15 | 16 | if newPools, ok := pools.(*resources.PoolsV1); ok { 17 | return *newPools, nil 18 | } 19 | return resources.PoolsV1{}, errors.New("Pool Index call error.") 20 | } 21 | 22 | // CreatePool a pool and returns the location. 23 | func (c *Client) CreatePool(poolToCreate resources.PoolV1) (string, error) { 24 | 25 | poolLocation, err := c.SendResource("POST", "/pools", &poolToCreate) 26 | if err != nil { 27 | return "", err 28 | } 29 | return poolLocation, nil 30 | } 31 | 32 | // CreateShowPool creates a pool and then returns that pool. 33 | func (c *Client) CreateShowPool(poolToCreate resources.PoolV1) (resources.PoolV1, error) { 34 | receivedPool, err := c.SendReceiveResource("POST", "GET", "/pools", &poolToCreate) 35 | if err != nil { 36 | return resources.PoolV1{}, err 37 | } 38 | if pool, ok := receivedPool.(*resources.PoolV1); ok { 39 | return *pool, nil 40 | } 41 | return resources.PoolV1{}, errors.New("CreateShowPool call error.") 42 | } 43 | 44 | // ShowPool returns the requested Pool. 45 | func (c *Client) ShowPool(poolID string, poolToShow resources.PoolV1) (resources.PoolV1, error) { 46 | receivedPool, err := c.ReceiveResource("GET", "/pools/"+poolID, poolToShow.Type(), poolToShow.Version()) 47 | if err != nil { 48 | return resources.PoolV1{}, err 49 | } 50 | if pool, ok := receivedPool.(*resources.PoolV1); ok { 51 | return *pool, nil 52 | } 53 | return resources.PoolV1{}, errors.New("Pools Show call error.") 54 | } 55 | 56 | // UpdatePool updates the requested Pool and returns its location. 57 | func (c *Client) UpdatePool(poolID string, poolToUpdate resources.PoolV1) (string, error) { 58 | location, err := c.SendResource("PATCH", "/pools/"+poolID, &poolToUpdate) 59 | if err != nil { 60 | return "", err 61 | } 62 | return location, nil 63 | } 64 | 65 | // UpdateShowPool updates a pool and then returns that pool. 66 | func (c *Client) UpdateShowPool(poolID string, poolToUpdate resources.PoolV1) (resources.PoolV1, error) { 67 | receivedPool, err := c.SendReceiveResource("PATCH", "GET", "/pools/"+poolID, &poolToUpdate) 68 | if err != nil { 69 | return resources.PoolV1{}, err 70 | } 71 | if pools, ok := receivedPool.(*resources.PoolV1); ok { 72 | return *pools, nil 73 | } 74 | return resources.PoolV1{}, errors.New("UpdateShowPool call error.") 75 | } 76 | 77 | // DeletePool removes the requested Pool and returns the location. 78 | func (c *Client) DeletePool(poolID string, poolToDelete resources.PoolV1) (string, error) { 79 | location, err := c.SendResource("DELETE", "/pools/"+poolID, &poolToDelete) 80 | if err != nil { 81 | return "", err 82 | } 83 | return location, nil 84 | } 85 | -------------------------------------------------------------------------------- /resources/subnet_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | . "github.com/RackHD/ipam/resources" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("SubnetCreator", func() { 13 | It("should return a SubnetV1 resource by default", func() { 14 | resource, err := SubnetCreator("") 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&SubnetV1{})) 17 | }) 18 | }) 19 | 20 | var _ = Describe("SubnetV1", func() { 21 | var ( 22 | resource = SubnetV1{ 23 | ID: bson.NewObjectId().Hex(), 24 | Name: "SubnetV1 Name", 25 | Tags: []string{"SubnetV1"}, 26 | Metadata: "SubnetV1 Metadata", 27 | } 28 | model = models.Subnet{ 29 | ID: bson.NewObjectId(), 30 | Name: "Subnet Name", 31 | Tags: []string{"Subnet"}, 32 | Metadata: "Subnet Metadata", 33 | } 34 | ) 35 | 36 | Describe("Type", func() { 37 | It("should return the correct resource type", func() { 38 | Expect(resource.Type()).To(Equal(SubnetResourceType)) 39 | }) 40 | }) 41 | 42 | Describe("Version", func() { 43 | It("should return the correct resource version", func() { 44 | Expect(resource.Version()).To(Equal(SubnetResourceVersionV1)) 45 | }) 46 | }) 47 | 48 | Describe("Marshal", func() { 49 | It("should copy the models.Subnet to itself", func() { 50 | err := resource.Marshal(model) 51 | Expect(err).ToNot(HaveOccurred()) 52 | 53 | Expect(resource.ID).To(Equal(model.ID.Hex())) 54 | Expect(resource.Name).To(Equal(model.Name)) 55 | Expect(resource.Tags).To(Equal(model.Tags)) 56 | Expect(resource.Metadata).To(Equal(model.Metadata)) 57 | }) 58 | 59 | It("should return an error if a model.Subnet is not provided", func() { 60 | err := resource.Marshal("invalid") 61 | Expect(err).To(HaveOccurred()) 62 | }) 63 | }) 64 | 65 | Describe("Unmarshal", func() { 66 | It("should copy itself to a models.Subnet", func() { 67 | m, err := resource.Unmarshal() 68 | Expect(err).ToNot(HaveOccurred()) 69 | Expect(m).To(BeAssignableToTypeOf(models.Subnet{})) 70 | 71 | if result, ok := m.(models.Subnet); ok { 72 | Expect(result.ID.Hex()).To(Equal(resource.ID)) 73 | Expect(result.Name).To(Equal(resource.Name)) 74 | Expect(result.Tags).To(Equal(resource.Tags)) 75 | Expect(result.Metadata).To(Equal(resource.Metadata)) 76 | } 77 | }) 78 | 79 | It("should generate a new object ID if one is not present", func() { 80 | resource.ID = "" 81 | 82 | m, err := resource.Unmarshal() 83 | Expect(err).ToNot(HaveOccurred()) 84 | Expect(m).To(BeAssignableToTypeOf(models.Subnet{})) 85 | 86 | if result, ok := m.(models.Subnet); ok { 87 | Expect(result.ID.Hex()).To(Equal(resource.ID)) 88 | Expect(result.Name).To(Equal(resource.Name)) 89 | Expect(result.Tags).To(Equal(resource.Tags)) 90 | Expect(result.Metadata).To(Equal(resource.Metadata)) 91 | } 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /ipam/reservations.go: -------------------------------------------------------------------------------- 1 | package ipam 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | // IpamCollectionReservations is the name of the Mongo collection which stores Reservations. 9 | const IpamCollectionReservations string = "reservations" 10 | 11 | // GetReservations returns a list of Reservations. 12 | func (ipam *Ipam) GetReservations(id string) ([]models.Reservation, error) { 13 | session := ipam.session.Copy() 14 | defer session.Close() 15 | 16 | var reservations []models.Reservation 17 | 18 | session.DB(IpamDatabase).C(IpamCollectionReservations).Find(bson.M{"subnet": bson.ObjectIdHex(id)}).All(&reservations) 19 | 20 | return reservations, nil 21 | } 22 | 23 | // GetReservation returns the requested Reservation. 24 | func (ipam *Ipam) GetReservation(id string) (models.Reservation, error) { 25 | session := ipam.session.Copy() 26 | defer session.Close() 27 | 28 | var reservation models.Reservation 29 | 30 | return reservation, session.DB(IpamDatabase).C(IpamCollectionReservations).Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&reservation) 31 | } 32 | 33 | // CreateReservation creates a Reservation. 34 | func (ipam *Ipam) CreateReservation(reservation models.Reservation) error { 35 | session := ipam.session.Copy() 36 | defer session.Close() 37 | 38 | err := session.DB(IpamDatabase).C(IpamCollectionReservations).Insert(reservation) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | err = session.DB(IpamDatabase).C(IpamCollectionLeases).Update( 44 | bson.M{"reservation": nil, "subnet": reservation.Subnet}, 45 | bson.M{"$set": bson.M{"reservation": reservation.ID}}, 46 | ) 47 | if err != nil { 48 | session.DB(IpamDatabase).C(IpamCollectionReservations).Remove(reservation) 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | 55 | // UpdateReservation updates a Reservation. 56 | func (ipam *Ipam) UpdateReservation(reservation models.Reservation) error { 57 | session := ipam.session.Copy() 58 | defer session.Close() 59 | 60 | return session.DB(IpamDatabase).C(IpamCollectionReservations).UpdateId(reservation.ID, reservation) 61 | } 62 | 63 | // DeleteReservation removes a Reservation. 64 | func (ipam *Ipam) DeleteReservation(id string) error { 65 | session := ipam.session.Copy() 66 | defer session.Close() 67 | 68 | err := session.DB(IpamDatabase).C(IpamCollectionLeases).Update( 69 | bson.M{"reservation": bson.ObjectIdHex(id)}, 70 | bson.M{"$set": bson.M{"reservation": nil}}, 71 | ) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | return session.DB(IpamDatabase).C(IpamCollectionReservations).RemoveId(bson.ObjectIdHex(id)) 77 | } 78 | 79 | // DeleteReservations remove all reservations in a subnet 80 | func (ipam *Ipam) DeleteReservations(id string) error { 81 | reservations, err := ipam.GetReservations(id) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | for _, reservation := range reservations { 87 | err := ipam.DeleteReservation(reservation.ID.Hex()) 88 | if err != nil { 89 | return err 90 | } 91 | } 92 | return nil 93 | } 94 | -------------------------------------------------------------------------------- /resources/reservation_test.go: -------------------------------------------------------------------------------- 1 | package resources_test 2 | 3 | import ( 4 | "github.com/RackHD/ipam/models" 5 | . "github.com/RackHD/ipam/resources" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("ReservationCreator", func() { 13 | It("should return a ReservationV1 resource by default", func() { 14 | resource, err := ReservationCreator("") 15 | Expect(err).ToNot(HaveOccurred()) 16 | Expect(resource).To(BeAssignableToTypeOf(&ReservationV1{})) 17 | }) 18 | }) 19 | 20 | var _ = Describe("ReservationV1", func() { 21 | var ( 22 | resource = ReservationV1{ 23 | ID: bson.NewObjectId().Hex(), 24 | Name: "ReservationV1 Name", 25 | Tags: []string{"ReservationV1"}, 26 | Metadata: "ReservationV1 Metadata", 27 | } 28 | model = models.Reservation{ 29 | ID: bson.NewObjectId(), 30 | Name: "Reservation Name", 31 | Tags: []string{"Reservation"}, 32 | Metadata: "Reservation Metadata", 33 | } 34 | ) 35 | 36 | Describe("Type", func() { 37 | It("should return the correct resource type", func() { 38 | Expect(resource.Type()).To(Equal(ReservationResourceType)) 39 | }) 40 | }) 41 | 42 | Describe("Version", func() { 43 | It("should return the correct resource version", func() { 44 | Expect(resource.Version()).To(Equal(ReservationResourceVersionV1)) 45 | }) 46 | }) 47 | 48 | Describe("Marshal", func() { 49 | It("should copy the models.Reservation to itself", func() { 50 | err := resource.Marshal(model) 51 | Expect(err).ToNot(HaveOccurred()) 52 | 53 | Expect(resource.ID).To(Equal(model.ID.Hex())) 54 | Expect(resource.Name).To(Equal(model.Name)) 55 | Expect(resource.Tags).To(Equal(model.Tags)) 56 | Expect(resource.Metadata).To(Equal(model.Metadata)) 57 | }) 58 | 59 | It("should return an error if a model.Reservation is not provided", func() { 60 | err := resource.Marshal("invalid") 61 | Expect(err).To(HaveOccurred()) 62 | }) 63 | }) 64 | 65 | Describe("Unmarshal", func() { 66 | It("should copy itself to a models.Reservation", func() { 67 | m, err := resource.Unmarshal() 68 | Expect(err).ToNot(HaveOccurred()) 69 | Expect(m).To(BeAssignableToTypeOf(models.Reservation{})) 70 | 71 | if result, ok := m.(models.Reservation); ok { 72 | Expect(result.ID.Hex()).To(Equal(resource.ID)) 73 | Expect(result.Name).To(Equal(resource.Name)) 74 | Expect(result.Tags).To(Equal(resource.Tags)) 75 | Expect(result.Metadata).To(Equal(resource.Metadata)) 76 | } 77 | }) 78 | 79 | It("should generate a new object ID if one is not present", func() { 80 | resource.ID = "" 81 | 82 | m, err := resource.Unmarshal() 83 | Expect(err).ToNot(HaveOccurred()) 84 | Expect(m).To(BeAssignableToTypeOf(models.Reservation{})) 85 | 86 | if result, ok := m.(models.Reservation); ok { 87 | Expect(result.ID.Hex()).To(Equal(resource.ID)) 88 | Expect(result.Name).To(Equal(resource.Name)) 89 | Expect(result.Tags).To(Equal(resource.Tags)) 90 | Expect(result.Metadata).To(Equal(resource.Metadata)) 91 | } 92 | }) 93 | }) 94 | }) 95 | -------------------------------------------------------------------------------- /client/subnets.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/RackHD/ipam/resources" 7 | ) 8 | 9 | // IndexSubnets returns a list of Subnets. 10 | func (c *Client) IndexSubnets(poolID string) (resources.SubnetsV1, error) { 11 | receivedSubnets, err := c.ReceiveResource("GET", "/pools/"+poolID+"/subnets", "", "") 12 | if err != nil { 13 | return resources.SubnetsV1{}, err 14 | } 15 | if subnets, ok := receivedSubnets.(*resources.SubnetsV1); ok { 16 | return *subnets, nil 17 | } 18 | return resources.SubnetsV1{}, errors.New("Subnet Index call error.") 19 | } 20 | 21 | // CreateSubnet a subnet and return the location. 22 | func (c *Client) CreateSubnet(poolID string, subnetToCreate resources.SubnetV1) (string, error) { 23 | subnetLocation, err := c.SendResource("POST", "/pools/"+poolID+"/subnets", &subnetToCreate) 24 | if err != nil { 25 | return "", err 26 | } 27 | return subnetLocation, nil 28 | } 29 | 30 | // CreateShowSubnet creates a subnet and then returns that subnet. 31 | func (c *Client) CreateShowSubnet(poolID string, subnetToCreate resources.SubnetV1) (resources.SubnetV1, error) { 32 | receivedSubnet, err := c.SendReceiveResource("POST", "GET", "/pools/"+poolID+"/subnets", &subnetToCreate) 33 | if err != nil { 34 | return resources.SubnetV1{}, err 35 | } 36 | if subnet, ok := receivedSubnet.(*resources.SubnetV1); ok { 37 | return *subnet, nil 38 | } 39 | return resources.SubnetV1{}, errors.New("CreateShowSubnet call error.") 40 | } 41 | 42 | // ShowSubnet returns the requested subnet. 43 | func (c *Client) ShowSubnet(subnetID string, subnetToGet resources.SubnetV1) (resources.SubnetV1, error) { 44 | receivedSubnet, err := c.ReceiveResource("GET", "/subnets/"+subnetID, subnetToGet.Type(), subnetToGet.Version()) 45 | if err != nil { 46 | return resources.SubnetV1{}, err 47 | } 48 | if subnet, ok := receivedSubnet.(*resources.SubnetV1); ok { 49 | return *subnet, nil 50 | } 51 | return resources.SubnetV1{}, errors.New("Subnet Show call error.") 52 | } 53 | 54 | // UpdateSubnet updates the requested subnet and returns its location. 55 | func (c *Client) UpdateSubnet(subnetID string, subnetToUpdate resources.SubnetV1) (string, error) { 56 | subnetLocation, err := c.SendResource("PATCH", "/subnets/"+subnetID, &subnetToUpdate) 57 | if err != nil { 58 | return "", err 59 | } 60 | return subnetLocation, nil 61 | } 62 | 63 | // UpdateShowSubnet updates a Subnet and then returns that Subnet. 64 | func (c *Client) UpdateShowSubnet(subnetID string, subnetToUpdate resources.SubnetV1) (resources.SubnetV1, error) { 65 | receivedSubnet, err := c.SendReceiveResource("PATCH", "GET", "/subnets/"+subnetID, &subnetToUpdate) 66 | if err != nil { 67 | return resources.SubnetV1{}, err 68 | } 69 | if subnet, ok := receivedSubnet.(*resources.SubnetV1); ok { 70 | return *subnet, nil 71 | } 72 | return resources.SubnetV1{}, errors.New("UpdateShowSubnet call error.") 73 | } 74 | 75 | // DeleteSubnet removed the requested subnet and returns the location. 76 | func (c *Client) DeleteSubnet(subnetID string, subnetToDelete resources.SubnetV1) (string, error) { 77 | subnetLocation, err := c.SendResource("DELETE", "/subnets/"+subnetID, &subnetToDelete) 78 | if err != nil { 79 | return "", err 80 | } 81 | return subnetLocation, nil 82 | } 83 | -------------------------------------------------------------------------------- /controllers/helpers/renderers.go: -------------------------------------------------------------------------------- 1 | package helpers 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "mime" 7 | "net/http" 8 | 9 | "github.com/RackHD/ipam/resources/factory" 10 | ) 11 | 12 | // HeaderContentType represents the HTTP Content-Type header. 13 | const HeaderContentType string = "Content-Type" 14 | 15 | // HeaderAccept represents the HTTP Accept header. 16 | const HeaderAccept string = "Accept" 17 | 18 | // HeaderLocation represents the HTTP Location header. 19 | const HeaderLocation string = "Location" 20 | 21 | // RenderError writes the error and associated error HTTP status code to the response. 22 | func RenderError(w http.ResponseWriter, err error) { 23 | if e, ok := err.(HTTPStatusError); ok { 24 | http.Error(w, e.Error(), e.HTTPStatus()) 25 | } else { 26 | http.Error(w, err.Error(), ErrorToHTTPStatus(err)) 27 | } 28 | } 29 | 30 | // RenderLocation writes the location and associated status code to the response. 31 | func RenderLocation(w http.ResponseWriter, status int, location string) error { 32 | w.Header().Add(HeaderLocation, location) 33 | w.WriteHeader(status) 34 | w.Write([]byte{}) 35 | 36 | return nil 37 | } 38 | 39 | // AcceptResource ... 40 | func AcceptResource(r *http.Request, expected string) (interface{}, error) { 41 | mediaType, err := NewMediaType(r.Header.Get(HeaderContentType)) 42 | if err != nil { 43 | return nil, NewHTTPStatusError( 44 | http.StatusUnsupportedMediaType, 45 | "Invalid Resource: %s", err, 46 | ) 47 | } 48 | 49 | if mediaType.Type != expected { 50 | return nil, NewHTTPStatusError( 51 | http.StatusUnsupportedMediaType, 52 | "Unsupported Resource Type: %s != %s", mediaType.Type, expected, 53 | ) 54 | } 55 | 56 | resource, err := factory.Require(mediaType.Type, mediaType.Version) 57 | if err != nil { 58 | return nil, NewHTTPStatusError( 59 | http.StatusUnsupportedMediaType, 60 | "Unsupported Resource Version: %s", err, 61 | ) 62 | } 63 | 64 | err = json.NewDecoder(r.Body).Decode(&resource) 65 | if err != nil { 66 | return nil, err 67 | } 68 | 69 | return resource.Unmarshal() 70 | } 71 | 72 | // RenderResource accepts HTTP request/response objects as well as an intended HTTP status code, 73 | // expected HTTP Content-Type, and a resource object to render. 74 | func RenderResource(w http.ResponseWriter, r *http.Request, expected string, status int, object interface{}) error { 75 | // Ignore parsing errors and set the expected default. 76 | mediaType, _ := NewMediaType(r.Header.Get(HeaderAccept)) 77 | if mediaType.Type != expected { 78 | mediaType.Type = expected 79 | } 80 | 81 | resource, err := factory.Request(mediaType.Type, mediaType.Version) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | err = resource.Marshal(object) 87 | if err != nil { 88 | return err 89 | } 90 | 91 | data, err := json.Marshal(resource) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | w.Header().Set( 97 | HeaderContentType, 98 | mime.FormatMediaType( 99 | fmt.Sprintf("%s+%s", resource.Type(), "json"), 100 | map[string]string{"version": resource.Version()}, 101 | ), 102 | ) 103 | 104 | w.WriteHeader(status) 105 | w.Write(data) 106 | 107 | return nil 108 | } 109 | -------------------------------------------------------------------------------- /controllers/pools.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "gopkg.in/mgo.v2/bson" 8 | 9 | "github.com/RackHD/ipam/controllers/helpers" 10 | "github.com/RackHD/ipam/interfaces" 11 | "github.com/RackHD/ipam/models" 12 | "github.com/RackHD/ipam/resources" 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | // PoolsController provides methods for handling requests to the Pools API. 17 | type PoolsController struct { 18 | ipam interfaces.Ipam 19 | } 20 | 21 | // NewPoolsController returns a newly configured PoolsController. 22 | func NewPoolsController(router *mux.Router, ipam interfaces.Ipam) (*PoolsController, error) { 23 | c := PoolsController{ 24 | ipam: ipam, 25 | } 26 | 27 | router.Handle("/pools", helpers.ErrorHandler(c.Index)).Methods(http.MethodGet) 28 | router.Handle("/pools", helpers.ErrorHandler(c.Create)).Methods(http.MethodPost) 29 | router.Handle("/pools/{id}", helpers.ErrorHandler(c.Show)).Methods(http.MethodGet) 30 | router.Handle("/pools/{id}", helpers.ErrorHandler(c.Update)).Methods(http.MethodPut, http.MethodPatch) 31 | router.Handle("/pools/{id}", helpers.ErrorHandler(c.Delete)).Methods(http.MethodDelete) 32 | 33 | return &c, nil 34 | } 35 | 36 | // Index returns a list of Pools. 37 | func (c *PoolsController) Index(w http.ResponseWriter, r *http.Request) error { 38 | pools, err := c.ipam.GetPools() 39 | if err != nil { 40 | return err 41 | } 42 | 43 | return helpers.RenderResource(w, r, resources.PoolsResourceType, http.StatusOK, pools) 44 | } 45 | 46 | // Create creates a Pool. 47 | func (c *PoolsController) Create(w http.ResponseWriter, r *http.Request) error { 48 | resource, err := helpers.AcceptResource(r, resources.PoolResourceType) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | if pool, ok := resource.(models.Pool); ok { 54 | err = c.ipam.CreatePool(pool) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | return helpers.RenderLocation(w, http.StatusCreated, fmt.Sprintf("/pools/%s", pool.ID.Hex())) 60 | } 61 | 62 | return fmt.Errorf("Invalid Resource Type") 63 | } 64 | 65 | // Show returns the requested Pool. 66 | func (c *PoolsController) Show(w http.ResponseWriter, r *http.Request) error { 67 | vars := mux.Vars(r) 68 | 69 | pool, err := c.ipam.GetPool(vars["id"]) 70 | if err != nil { 71 | return err 72 | } 73 | 74 | return helpers.RenderResource(w, r, resources.PoolResourceType, http.StatusOK, pool) 75 | } 76 | 77 | // Update updates the requested Pool. 78 | func (c *PoolsController) Update(w http.ResponseWriter, r *http.Request) error { 79 | vars := mux.Vars(r) 80 | 81 | resource, err := helpers.AcceptResource(r, resources.PoolResourceType) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | if pool, ok := resource.(models.Pool); ok { 87 | pool.ID = bson.ObjectIdHex(vars["id"]) 88 | 89 | err = c.ipam.UpdatePool(pool) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return helpers.RenderLocation(w, http.StatusNoContent, fmt.Sprintf("/pools/%s", pool.ID.Hex())) 95 | } 96 | 97 | return fmt.Errorf("Invalid Resource Type") 98 | } 99 | 100 | // Delete removes the requested Pool. 101 | func (c *PoolsController) Delete(w http.ResponseWriter, r *http.Request) error { 102 | vars := mux.Vars(r) 103 | 104 | err := c.ipam.DeletePool(vars["id"]) 105 | if err != nil { 106 | return err 107 | } 108 | 109 | return helpers.RenderLocation(w, http.StatusOK, fmt.Sprintf("/pools")) 110 | } 111 | -------------------------------------------------------------------------------- /controllers/subnets.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "gopkg.in/mgo.v2/bson" 8 | 9 | "github.com/RackHD/ipam/controllers/helpers" 10 | "github.com/RackHD/ipam/interfaces" 11 | "github.com/RackHD/ipam/models" 12 | "github.com/RackHD/ipam/resources" 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | // SubnetsController provides methods for handling requests to the Subnets API. 17 | type SubnetsController struct { 18 | ipam interfaces.Ipam 19 | } 20 | 21 | // NewSubnetsController returns a newly configured SubnetsController. 22 | func NewSubnetsController(router *mux.Router, ipam interfaces.Ipam) (*SubnetsController, error) { 23 | c := SubnetsController{ 24 | ipam: ipam, 25 | } 26 | 27 | router.Handle("/pools/{id}/subnets", helpers.ErrorHandler(c.Index)).Methods(http.MethodGet) 28 | router.Handle("/pools/{id}/subnets", helpers.ErrorHandler(c.Create)).Methods(http.MethodPost) 29 | router.Handle("/subnets/{id}", helpers.ErrorHandler(c.Show)).Methods(http.MethodGet) 30 | router.Handle("/subnets/{id}", helpers.ErrorHandler(c.Update)).Methods(http.MethodPut, http.MethodPatch) 31 | router.Handle("/subnets/{id}", helpers.ErrorHandler(c.Delete)).Methods(http.MethodDelete) 32 | 33 | return &c, nil 34 | } 35 | 36 | // Index returns a list of Subnets. 37 | func (c *SubnetsController) Index(w http.ResponseWriter, r *http.Request) error { 38 | vars := mux.Vars(r) 39 | 40 | subnets, err := c.ipam.GetSubnets(vars["id"]) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return helpers.RenderResource(w, r, resources.SubnetsResourceType, http.StatusOK, subnets) 46 | } 47 | 48 | // Create creates a Subnet. 49 | func (c *SubnetsController) Create(w http.ResponseWriter, r *http.Request) error { 50 | vars := mux.Vars(r) 51 | 52 | resource, err := helpers.AcceptResource(r, resources.SubnetResourceType) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if subnet, ok := resource.(models.Subnet); ok { 58 | subnet.Pool = bson.ObjectIdHex(vars["id"]) 59 | 60 | err = c.ipam.CreateSubnet(subnet) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return helpers.RenderLocation(w, http.StatusCreated, fmt.Sprintf("/subnets/%s", subnet.ID.Hex())) 66 | } 67 | 68 | return fmt.Errorf("Invalid Resource Type") 69 | } 70 | 71 | // Show returns the requested Subnet. 72 | func (c *SubnetsController) Show(w http.ResponseWriter, r *http.Request) error { 73 | vars := mux.Vars(r) 74 | 75 | subnet, err := c.ipam.GetSubnet(vars["id"]) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return helpers.RenderResource(w, r, resources.SubnetResourceType, http.StatusOK, subnet) 81 | } 82 | 83 | // Update updates the requested Subnet. 84 | func (c *SubnetsController) Update(w http.ResponseWriter, r *http.Request) error { 85 | vars := mux.Vars(r) 86 | 87 | resource, err := helpers.AcceptResource(r, resources.SubnetResourceType) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if subnet, ok := resource.(models.Subnet); ok { 93 | subnet.ID = bson.ObjectIdHex(vars["id"]) 94 | 95 | err = c.ipam.UpdateSubnet(subnet) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return helpers.RenderLocation(w, http.StatusNoContent, fmt.Sprintf("/subnets/%s", subnet.ID.Hex())) 101 | } 102 | 103 | return fmt.Errorf("Invalid Resource Type") 104 | } 105 | 106 | // Delete removes the requested Subnet. 107 | func (c *SubnetsController) Delete(w http.ResponseWriter, r *http.Request) error { 108 | vars := mux.Vars(r) 109 | 110 | err := c.ipam.DeleteSubnet(vars["id"]) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return helpers.RenderLocation(w, http.StatusOK, fmt.Sprintf("/subnets")) 116 | } 117 | -------------------------------------------------------------------------------- /client/reservations.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/RackHD/ipam/resources" 7 | ) 8 | 9 | // IndexReservations returns a list of Reservations. 10 | func (c *Client) IndexReservations(subnetID string) (resources.ReservationsV1, error) { 11 | receivedReservations, err := c.ReceiveResource("GET", "/subnets/"+subnetID+"/reservations", "", "") 12 | if err != nil { 13 | return resources.ReservationsV1{}, err 14 | } 15 | if reservations, ok := receivedReservations.(*resources.ReservationsV1); ok { 16 | return *reservations, nil 17 | } 18 | return resources.ReservationsV1{}, errors.New("Reservation Index call error.") 19 | } 20 | 21 | // CreateReservation a Reservation and return the location. 22 | func (c *Client) CreateReservation(subnetID string, reservationToCreate resources.ReservationV1) (string, error) { 23 | reservationLocation, err := c.SendResource("POST", "/subnets/"+subnetID+"/reservations", &reservationToCreate) 24 | if err != nil { 25 | return "", err 26 | } 27 | return reservationLocation, nil 28 | } 29 | 30 | // CreateShowReservation creates a Reservation and then returns that Reservation. 31 | func (c *Client) CreateShowReservation(subnetID string, reservationToCreate resources.ReservationV1) (resources.ReservationV1, error) { 32 | receivedReservation, err := c.SendReceiveResource("POST", "GET", "/subnets/"+subnetID+"/reservations", &reservationToCreate) 33 | if err != nil { 34 | return resources.ReservationV1{}, err 35 | } 36 | if reservation, ok := receivedReservation.(*resources.ReservationV1); ok { 37 | return *reservation, nil 38 | } 39 | return resources.ReservationV1{}, errors.New("CreateShowReservation call error.") 40 | } 41 | 42 | // ShowReservation returns the requested Reservation. 43 | func (c *Client) ShowReservation(reservationID string, reservationToShow resources.ReservationV1) (resources.ReservationV1, error) { 44 | receivedReservation, err := c.ReceiveResource("GET", "/reservations/"+reservationID, reservationToShow.Type(), reservationToShow.Version()) 45 | if err != nil { 46 | return resources.ReservationV1{}, err 47 | } 48 | if reservation, ok := receivedReservation.(*resources.ReservationV1); ok { 49 | return *reservation, nil 50 | } 51 | return resources.ReservationV1{}, errors.New("Reservation Show call error.") 52 | } 53 | 54 | // UpdateReservation updates the requested Reservation and returns its location. 55 | func (c *Client) UpdateReservation(reservationID string, reservationToUpdate resources.ReservationV1) (string, error) { 56 | reservationLocation, err := c.SendResource("PATCH", "/reservations/"+reservationID, &reservationToUpdate) 57 | if err != nil { 58 | return "", err 59 | } 60 | return reservationLocation, nil 61 | } 62 | 63 | // UpdateShowReservation updates a Reservation and then returns that Reservation. 64 | func (c *Client) UpdateShowReservation(reservationID string, reservationToUpdate resources.ReservationV1) (resources.ReservationV1, error) { 65 | receivedReservation, err := c.SendReceiveResource("PATCH", "GET", "/reservations/"+reservationID, &reservationToUpdate) 66 | if err != nil { 67 | return resources.ReservationV1{}, err 68 | } 69 | if reservation, ok := receivedReservation.(*resources.ReservationV1); ok { 70 | return *reservation, nil 71 | } 72 | return resources.ReservationV1{}, errors.New("UpdateShowReservation call error.") 73 | } 74 | 75 | // DeleteReservation removed the requested Reservation and returns the location. 76 | func (c *Client) DeleteReservation(reservationID string, reservationToDelete resources.ReservationV1) (string, error) { 77 | reservationLocation, err := c.SendResource("DELETE", "/reservations/"+reservationID, &reservationToDelete) 78 | if err != nil { 79 | return "", err 80 | } 81 | return reservationLocation, nil 82 | } 83 | -------------------------------------------------------------------------------- /ipam/subnets.go: -------------------------------------------------------------------------------- 1 | package ipam 2 | 3 | import ( 4 | "encoding/binary" 5 | "fmt" 6 | 7 | "github.com/RackHD/ipam/models" 8 | "gopkg.in/mgo.v2/bson" 9 | ) 10 | 11 | // IpamCollectionSubnets is the name of the Mongo collection which stores Subnets. 12 | const IpamCollectionSubnets string = "subnets" 13 | 14 | // GetSubnets returns a list of Subnets. 15 | func (ipam *Ipam) GetSubnets(id string) ([]models.Subnet, error) { 16 | session := ipam.session.Copy() 17 | defer session.Close() 18 | 19 | var subnets []models.Subnet 20 | 21 | session.DB(IpamDatabase).C(IpamCollectionSubnets).Find(bson.M{"pool": bson.ObjectIdHex(id)}).All(&subnets) 22 | 23 | return subnets, nil 24 | } 25 | 26 | // GetSubnet returns the requested Subnet. 27 | func (ipam *Ipam) GetSubnet(id string) (models.Subnet, error) { 28 | session := ipam.session.Copy() 29 | defer session.Close() 30 | 31 | var subnet models.Subnet 32 | 33 | return subnet, session.DB(IpamDatabase).C(IpamCollectionSubnets).Find(bson.M{"_id": bson.ObjectIdHex(id)}).One(&subnet) 34 | } 35 | 36 | // CreateSubnet creates a Subnet. 37 | func (ipam *Ipam) CreateSubnet(subnet models.Subnet) error { 38 | session := ipam.session.Copy() 39 | defer session.Close() 40 | 41 | err := session.DB(IpamDatabase).C(IpamCollectionSubnets).Insert(subnet) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | // Convert byte arrays to integers (IPv4 only). 47 | start := binary.BigEndian.Uint32(subnet.Start.Data[len(subnet.Start.Data)-4:]) 48 | end := binary.BigEndian.Uint32(subnet.End.Data[len(subnet.End.Data)-4:]) 49 | 50 | // Iterate through the range of IP's and insert a record for each. 51 | for ; start <= end; start++ { 52 | // IP's are stored as 16 byte arrays and we're only doing IPv4 so prepend 53 | // the net.IP prefix that denotes an IPv4 address. 54 | prefix := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff} 55 | address := make([]byte, 4) 56 | 57 | binary.BigEndian.PutUint32(address, start) 58 | 59 | // Create the lease record, tie it to the subnet. 60 | lease := models.Lease{ 61 | ID: bson.NewObjectId(), 62 | Subnet: subnet.ID, 63 | Address: bson.Binary{ 64 | Kind: 0, 65 | Data: append(prefix, address...), 66 | }, 67 | } 68 | 69 | // Insert, though if we fail one we will have a partially populated subnet pool. 70 | err := session.DB(IpamDatabase).C(IpamCollectionLeases).Insert(lease) 71 | if err != nil { 72 | return err 73 | } 74 | } 75 | 76 | return nil 77 | } 78 | 79 | // UpdateSubnet updates a Subnet. 80 | func (ipam *Ipam) UpdateSubnet(subnet models.Subnet) error { 81 | return fmt.Errorf("UpdateSubnet Temporarily Disabled.") 82 | 83 | // session := ipam.session.Copy() 84 | // defer session.Close() 85 | // 86 | // return session.DB(IpamDatabase).C(IpamCollectionSubnets).UpdateId(subnet.ID, subnet) 87 | } 88 | 89 | // DeleteSubnet removes a Subnet and all revervations and leases associated to it 90 | func (ipam *Ipam) DeleteSubnet(id string) error { 91 | session := ipam.session.Copy() 92 | defer session.Close() 93 | 94 | err := ipam.DeleteReservations(id) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | err = ipam.DeleteLeases(id) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | return session.DB(IpamDatabase).C(IpamCollectionSubnets).RemoveId(bson.ObjectIdHex(id)) 105 | } 106 | 107 | // DeleteSubnets removes all subnet in a pool 108 | func (ipam *Ipam) DeleteSubnets(id string) error { 109 | subnets, err := ipam.GetSubnets(id) 110 | if err != nil { 111 | return err 112 | } 113 | 114 | for _, subnet := range subnets { 115 | err := ipam.DeleteSubnet(subnet.ID.Hex()) 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | return nil 121 | } 122 | -------------------------------------------------------------------------------- /controllers/reservations.go: -------------------------------------------------------------------------------- 1 | package controllers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "gopkg.in/mgo.v2/bson" 8 | 9 | "github.com/RackHD/ipam/controllers/helpers" 10 | "github.com/RackHD/ipam/interfaces" 11 | "github.com/RackHD/ipam/models" 12 | "github.com/RackHD/ipam/resources" 13 | "github.com/gorilla/mux" 14 | ) 15 | 16 | // ReservationsController provides methods for handling requests to the Reservations API. 17 | type ReservationsController struct { 18 | ipam interfaces.Ipam 19 | } 20 | 21 | // NewReservationsController returns a newly configured ReservationsController. 22 | func NewReservationsController(router *mux.Router, ipam interfaces.Ipam) (*ReservationsController, error) { 23 | c := ReservationsController{ 24 | ipam: ipam, 25 | } 26 | 27 | router.Handle("/subnets/{id}/reservations", helpers.ErrorHandler(c.Index)).Methods(http.MethodGet) 28 | router.Handle("/subnets/{id}/reservations", helpers.ErrorHandler(c.Create)).Methods(http.MethodPost) 29 | router.Handle("/reservations/{id}", helpers.ErrorHandler(c.Show)).Methods(http.MethodGet) 30 | router.Handle("/reservations/{id}", helpers.ErrorHandler(c.Update)).Methods(http.MethodPut, http.MethodPatch) 31 | router.Handle("/reservations/{id}", helpers.ErrorHandler(c.Delete)).Methods(http.MethodDelete) 32 | 33 | return &c, nil 34 | } 35 | 36 | // Index returns a list of Reservations. 37 | func (c *ReservationsController) Index(w http.ResponseWriter, r *http.Request) error { 38 | vars := mux.Vars(r) 39 | 40 | reservations, err := c.ipam.GetReservations(vars["id"]) 41 | if err != nil { 42 | return err 43 | } 44 | 45 | return helpers.RenderResource(w, r, resources.ReservationsResourceType, http.StatusOK, reservations) 46 | } 47 | 48 | // Create creates a Reservation. 49 | func (c *ReservationsController) Create(w http.ResponseWriter, r *http.Request) error { 50 | vars := mux.Vars(r) 51 | 52 | resource, err := helpers.AcceptResource(r, resources.ReservationResourceType) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | if reservation, ok := resource.(models.Reservation); ok { 58 | reservation.Subnet = bson.ObjectIdHex(vars["id"]) 59 | 60 | err = c.ipam.CreateReservation(reservation) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return helpers.RenderLocation(w, http.StatusCreated, fmt.Sprintf("/reservations/%s", reservation.ID.Hex())) 66 | } 67 | 68 | return fmt.Errorf("Invalid Resource Type") 69 | } 70 | 71 | // Show returns the requested Reservation. 72 | func (c *ReservationsController) Show(w http.ResponseWriter, r *http.Request) error { 73 | vars := mux.Vars(r) 74 | 75 | reservation, err := c.ipam.GetReservation(vars["id"]) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return helpers.RenderResource(w, r, resources.ReservationResourceType, http.StatusOK, reservation) 81 | } 82 | 83 | // Update updates the requested Reservation. 84 | func (c *ReservationsController) Update(w http.ResponseWriter, r *http.Request) error { 85 | vars := mux.Vars(r) 86 | 87 | resource, err := helpers.AcceptResource(r, resources.ReservationResourceType) 88 | if err != nil { 89 | return err 90 | } 91 | 92 | if reservation, ok := resource.(models.Reservation); ok { 93 | reservation.ID = bson.ObjectIdHex(vars["id"]) 94 | 95 | err = c.ipam.UpdateReservation(reservation) 96 | if err != nil { 97 | return err 98 | } 99 | 100 | return helpers.RenderLocation(w, http.StatusNoContent, fmt.Sprintf("/reservations/%s", reservation.ID.Hex())) 101 | } 102 | 103 | return fmt.Errorf("Invalid Resource Type") 104 | } 105 | 106 | // Delete removes the requested Reservation. 107 | func (c *ReservationsController) Delete(w http.ResponseWriter, r *http.Request) error { 108 | vars := mux.Vars(r) 109 | 110 | err := c.ipam.DeleteReservation(vars["id"]) 111 | if err != nil { 112 | return err 113 | } 114 | 115 | return helpers.RenderLocation(w, http.StatusOK, fmt.Sprintf("/reservations")) 116 | } 117 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "log" 10 | "mime" 11 | "net/http" 12 | "time" 13 | 14 | "github.com/RackHD/ipam/controllers/helpers" 15 | "github.com/RackHD/ipam/interfaces" 16 | "github.com/RackHD/ipam/resources/factory" 17 | "github.com/hashicorp/go-cleanhttp" 18 | ) 19 | 20 | // Client struct is used to configure the creation of a client 21 | type Client struct { 22 | Address string 23 | Scheme string 24 | } 25 | 26 | // NewClient returns a new client 27 | func NewClient(address string) *Client { 28 | // bootstrap the config 29 | c := &Client{ 30 | Address: address, 31 | Scheme: "http", 32 | } 33 | 34 | // Make sure IPAM connection is alive, with retries 35 | for i := 0; i < 5; i++ { 36 | _, err := c.IndexPools() 37 | if err == nil { 38 | return c 39 | } 40 | log.Println("Could not connect to IPAM, retrying in 5 Seconds...") 41 | time.Sleep(5 * time.Second) 42 | } 43 | 44 | return nil 45 | } 46 | 47 | // SendResource is used to send a generic resource type 48 | func (c *Client) SendResource(method, path string, in interfaces.Resource) (string, error) { 49 | 50 | body, err := encodeBody(in) 51 | if err != nil { 52 | return "", err 53 | } 54 | req, err := http.NewRequest(method, c.Scheme+"://"+c.Address+path, body) 55 | if err != nil { 56 | return "", err 57 | } 58 | req.Header.Set( 59 | "Content-Type", 60 | mime.FormatMediaType( 61 | fmt.Sprintf("%s+%s", in.Type(), "json"), 62 | map[string]string{"version": in.Version()}, 63 | ), 64 | ) 65 | 66 | client := cleanhttp.DefaultClient() 67 | resp, err := client.Do(req) 68 | if err != nil { 69 | return "", err 70 | } 71 | if resp.StatusCode < 200 || resp.StatusCode > 300 { 72 | return "", errors.New(resp.Status) 73 | } 74 | 75 | return resp.Header.Get("Location"), nil 76 | } 77 | 78 | // ReceiveResource is used to receive the passed reasource type 79 | func (c *Client) ReceiveResource(method, path, resourceType, resourceVersion string) (interfaces.Resource, error) { 80 | 81 | req, err := http.NewRequest(method, c.Scheme+"://"+c.Address+path, nil) 82 | 83 | req.Header.Set( 84 | "Content-Type", 85 | mime.FormatMediaType( 86 | fmt.Sprintf("%s+%s", resourceType, "json"), 87 | map[string]string{"version": resourceVersion}, 88 | ), 89 | ) 90 | 91 | client := cleanhttp.DefaultClient() 92 | resp, err := client.Do(req) 93 | if err != nil { 94 | return nil, err 95 | } 96 | 97 | mediaType, err := helpers.NewMediaType(resp.Header.Get("Content-Type")) 98 | if err != nil { 99 | return nil, err 100 | } 101 | resource, err := factory.Require(mediaType.Type, mediaType.Version) 102 | if err != nil { 103 | return nil, err 104 | } 105 | 106 | err = decodeBody(resp, &resource) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | return resource, nil 112 | } 113 | 114 | // SendReceiveResource is used to send a resource type and then 115 | // upon success, fetch and recieve that resource type 116 | func (c *Client) SendReceiveResource(methodSend, methodReceive, path string, in interfaces.Resource) (interfaces.Resource, error) { 117 | 118 | location, err := c.SendResource(methodSend, path, in) 119 | if err != nil { 120 | return nil, err 121 | } 122 | out, err := c.ReceiveResource(methodReceive, location, "", "") 123 | return out, err 124 | } 125 | 126 | // decodeBody is used to JSON decode a body 127 | func decodeBody(resp *http.Response, out interface{}) error { 128 | dec := json.NewDecoder(resp.Body) 129 | return dec.Decode(out) 130 | } 131 | 132 | // encodeBody is used to encode a request body 133 | func encodeBody(obj interface{}) (io.Reader, error) { 134 | if obj == nil { 135 | return nil, nil 136 | } 137 | buf := bytes.NewBuffer(nil) 138 | enc := json.NewEncoder(buf) 139 | if err := enc.Encode(obj); err != nil { 140 | return nil, err 141 | } 142 | return buf, nil 143 | } 144 | -------------------------------------------------------------------------------- /controllers/controllers_suite_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "io" 5 | "net/http" 6 | 7 | "github.com/RackHD/ipam/models" 8 | . "github.com/onsi/ginkgo" 9 | . "github.com/onsi/gomega" 10 | "gopkg.in/mgo.v2/bson" 11 | 12 | "testing" 13 | ) 14 | 15 | func TestControllers(t *testing.T) { 16 | RegisterFailHandler(Fail) 17 | RunSpecs(t, "Controllers Suite") 18 | } 19 | 20 | func NewRequest(method, url string, body io.Reader) *http.Request { 21 | req, err := http.NewRequest(method, url, body) 22 | Expect(err).NotTo(HaveOccurred()) 23 | return req 24 | } 25 | 26 | func Do(req *http.Request) *http.Response { 27 | client := &http.Client{} 28 | 29 | res, err := client.Do(req) 30 | Expect(err).NotTo(HaveOccurred()) 31 | 32 | return res 33 | } 34 | 35 | type MockIpam struct { 36 | Err error 37 | 38 | Pools []models.Pool 39 | PoolCreated models.Pool 40 | PoolUpdated models.Pool 41 | PoolDeleted string 42 | 43 | Subnets []models.Subnet 44 | SubnetCreated models.Subnet 45 | SubnetUpdated models.Subnet 46 | SubnetDeleted string 47 | 48 | Reservations []models.Reservation 49 | ReservationCreated models.Reservation 50 | ReservationUpdated models.Reservation 51 | ReservationDeleted string 52 | 53 | Leases []models.Lease 54 | LeaseUpdated models.Lease 55 | } 56 | 57 | func NewMockIpam() *MockIpam { 58 | return &MockIpam{ 59 | Err: nil, 60 | Pools: []models.Pool{ 61 | { 62 | ID: bson.ObjectIdHex("578af30bbc63780007d99195"), 63 | Name: "Mock Pool", 64 | Tags: []string{"mock"}, 65 | }, 66 | }, 67 | Subnets: []models.Subnet{ 68 | { 69 | ID: bson.ObjectIdHex("578af30bbc63780007d99195"), 70 | Name: "Mock Subnet", 71 | Tags: []string{"mock"}, 72 | Pool: bson.ObjectIdHex("578af30bbc63780007d99195"), 73 | }, 74 | }, 75 | Reservations: []models.Reservation{ 76 | { 77 | ID: bson.ObjectIdHex("578af30bbc63780007d99195"), 78 | Name: "Mock Subnet", 79 | Tags: []string{"mock"}, 80 | Subnet: bson.ObjectIdHex("578af30bbc63780007d99195"), 81 | }, 82 | }, 83 | } 84 | } 85 | 86 | // GetPools ... 87 | func (mock *MockIpam) GetPools() ([]models.Pool, error) { 88 | if mock.Err != nil { 89 | return []models.Pool{}, mock.Err 90 | } 91 | 92 | return mock.Pools, mock.Err 93 | } 94 | 95 | // GetPool ... 96 | func (mock *MockIpam) GetPool(id string) (models.Pool, error) { 97 | if mock.Err != nil { 98 | return models.Pool{}, mock.Err 99 | } 100 | 101 | return mock.Pools[0], mock.Err 102 | } 103 | 104 | // CreatePool ... 105 | func (mock *MockIpam) CreatePool(pool models.Pool) error { 106 | if mock.Err != nil { 107 | return mock.Err 108 | } 109 | 110 | mock.PoolCreated = pool 111 | 112 | return mock.Err 113 | } 114 | 115 | // UpdatePool ... 116 | func (mock *MockIpam) UpdatePool(pool models.Pool) error { 117 | if mock.Err != nil { 118 | return mock.Err 119 | } 120 | 121 | mock.PoolUpdated = pool 122 | 123 | return mock.Err 124 | } 125 | 126 | // DeletePool ... 127 | func (mock *MockIpam) DeletePool(id string) error { 128 | if mock.Err != nil { 129 | return mock.Err 130 | } 131 | 132 | mock.PoolDeleted = id 133 | 134 | return mock.Err 135 | } 136 | 137 | // GetSubnets ... 138 | func (mock *MockIpam) GetSubnets(string) ([]models.Subnet, error) { 139 | if mock.Err != nil { 140 | return []models.Subnet{}, mock.Err 141 | } 142 | 143 | return mock.Subnets, mock.Err 144 | } 145 | 146 | // GetSubnet ... 147 | func (mock *MockIpam) GetSubnet(string) (models.Subnet, error) { 148 | if mock.Err != nil { 149 | return models.Subnet{}, mock.Err 150 | } 151 | 152 | return mock.Subnets[0], mock.Err 153 | } 154 | 155 | // CreateSubnet ... 156 | func (mock *MockIpam) CreateSubnet(subnet models.Subnet) error { 157 | if mock.Err != nil { 158 | return mock.Err 159 | } 160 | 161 | mock.SubnetCreated = subnet 162 | 163 | return mock.Err 164 | } 165 | 166 | // UpdateSubnet ... 167 | func (mock *MockIpam) UpdateSubnet(subnet models.Subnet) error { 168 | if mock.Err != nil { 169 | return mock.Err 170 | } 171 | 172 | mock.SubnetUpdated = subnet 173 | 174 | return mock.Err 175 | } 176 | 177 | // DeleteSubnet ... 178 | func (mock *MockIpam) DeleteSubnet(id string) error { 179 | if mock.Err != nil { 180 | return mock.Err 181 | } 182 | 183 | mock.SubnetDeleted = id 184 | 185 | return mock.Err 186 | } 187 | 188 | // GetReservations ... 189 | func (mock *MockIpam) GetReservations(string) ([]models.Reservation, error) { 190 | if mock.Err != nil { 191 | return []models.Reservation{}, mock.Err 192 | } 193 | 194 | return mock.Reservations, mock.Err 195 | } 196 | 197 | // GetReservation ... 198 | func (mock *MockIpam) GetReservation(string) (models.Reservation, error) { 199 | if mock.Err != nil { 200 | return models.Reservation{}, mock.Err 201 | } 202 | 203 | return mock.Reservations[0], mock.Err 204 | } 205 | 206 | // CreateReservation ... 207 | func (mock *MockIpam) CreateReservation(reservation models.Reservation) error { 208 | if mock.Err != nil { 209 | return mock.Err 210 | } 211 | 212 | mock.ReservationCreated = reservation 213 | 214 | return mock.Err 215 | } 216 | 217 | // UpdateReservation ... 218 | func (mock *MockIpam) UpdateReservation(reservation models.Reservation) error { 219 | if mock.Err != nil { 220 | return mock.Err 221 | } 222 | 223 | mock.ReservationUpdated = reservation 224 | 225 | return mock.Err 226 | } 227 | 228 | // DeleteReservation ... 229 | func (mock *MockIpam) DeleteReservation(id string) error { 230 | if mock.Err != nil { 231 | return mock.Err 232 | } 233 | 234 | mock.ReservationDeleted = id 235 | 236 | return mock.Err 237 | } 238 | 239 | // GetPoolReservations ... 240 | func (mock *MockIpam) GetPoolReservations(string) ([]models.Reservation, error) { 241 | if mock.Err != nil { 242 | return []models.Reservation{}, mock.Err 243 | } 244 | 245 | return mock.Reservations, mock.Err 246 | } 247 | 248 | // GetReservations ... 249 | func (mock *MockIpam) GetLeases(string) ([]models.Lease, error) { 250 | if mock.Err != nil { 251 | return []models.Lease{}, mock.Err 252 | } 253 | 254 | return mock.Leases, mock.Err 255 | } 256 | 257 | // GetReservation ... 258 | func (mock *MockIpam) GetLease(string) (models.Lease, error) { 259 | if mock.Err != nil { 260 | return models.Lease{}, mock.Err 261 | } 262 | 263 | return mock.Leases[0], mock.Err 264 | } 265 | 266 | // UpdateReservation ... 267 | func (mock *MockIpam) UpdateLease(lease models.Lease) error { 268 | if mock.Err != nil { 269 | return mock.Err 270 | } 271 | 272 | mock.LeaseUpdated = lease 273 | 274 | return mock.Err 275 | } 276 | -------------------------------------------------------------------------------- /client/client_test.go: -------------------------------------------------------------------------------- 1 | package client_test 2 | 3 | import ( 4 | . "github.com/RackHD/ipam/client" 5 | "github.com/RackHD/ipam/resources" 6 | 7 | . "github.com/onsi/ginkgo" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Client tests", func() { 12 | var ipamAddress, start, end string 13 | var err error 14 | 15 | BeforeEach(func() { 16 | ipamAddress = "127.0.0.1:8000" 17 | start = "192.168.1.10" 18 | end = "192.168.1.11" 19 | }) 20 | var _ = Describe("Pools tests", func() { 21 | var ipamClient *Client 22 | var pool resources.PoolV1 23 | 24 | BeforeEach(func() { 25 | ipamClient = NewClient(ipamAddress) 26 | Expect(ipamClient).ToNot(BeNil()) 27 | }) 28 | 29 | AfterEach(func() { 30 | poolLocation, err := ipamClient.DeletePool(pool.ID, pool) 31 | Expect(err).To(BeNil()) 32 | Expect(poolLocation).To(Equal("/pools")) 33 | }) 34 | It("Should create a pool and return that pool object", func() { 35 | 36 | pool = resources.PoolV1{ 37 | Name: "Pool1", 38 | Metadata: "yodawg I heard you like interfaces", 39 | } 40 | 41 | pool, err = ipamClient.CreateShowPool(pool) 42 | Expect(err).To(BeNil()) 43 | Expect(pool.ID).ToNot(Equal("")) 44 | Expect(pool.Name).To(Equal("Pool1")) 45 | 46 | }) 47 | }) 48 | 49 | var _ = Describe("Subnets tests", func() { 50 | var ipamClient *Client 51 | var pool resources.PoolV1 52 | var subnet resources.SubnetV1 53 | 54 | BeforeEach(func() { 55 | ipamClient = NewClient(ipamAddress) 56 | Expect(ipamClient).ToNot(BeNil()) 57 | 58 | pool = resources.PoolV1{ 59 | Name: "SubnetTestPool1", 60 | } 61 | 62 | pool, err = ipamClient.CreateShowPool(pool) 63 | Expect(err).To(BeNil()) 64 | Expect(pool.ID).ToNot(Equal("")) 65 | Expect(pool.Name).To(Equal("SubnetTestPool1")) 66 | 67 | }) 68 | 69 | AfterEach(func() { 70 | poolLocation, err := ipamClient.DeletePool(pool.ID, pool) 71 | Expect(err).To(BeNil()) 72 | Expect(poolLocation).To(Equal("/pools")) 73 | }) 74 | 75 | It("Should create a subnet and return that subnet object", func() { 76 | 77 | subnet = resources.SubnetV1{ 78 | Name: "Subnet1", 79 | Pool: pool.ID, 80 | Start: start, 81 | End: end, 82 | } 83 | 84 | subnet, err = ipamClient.CreateShowSubnet(pool.ID, subnet) 85 | Expect(err).To(BeNil()) 86 | Expect(subnet.ID).ToNot(Equal("")) 87 | Expect(subnet.Name).To(Equal("Subnet1")) 88 | Expect(subnet.Pool).To(Equal(pool.ID)) 89 | 90 | }) 91 | 92 | }) 93 | var _ = Describe("Reservations tests", func() { 94 | 95 | var ipamClient *Client 96 | var pool resources.PoolV1 97 | var subnet resources.SubnetV1 98 | var reservation resources.ReservationV1 99 | 100 | BeforeEach(func() { 101 | ipamClient = NewClient(ipamAddress) 102 | Expect(ipamClient).ToNot(BeNil()) 103 | 104 | pool = resources.PoolV1{ 105 | Name: "ReservationTestPool1", 106 | } 107 | 108 | pool, err = ipamClient.CreateShowPool(pool) 109 | Expect(err).To(BeNil()) 110 | Expect(pool.ID).ToNot(Equal("")) 111 | Expect(pool.Name).To(Equal("ReservationTestPool1")) 112 | 113 | subnet = resources.SubnetV1{ 114 | Name: "ReservationTestSubnet1", 115 | Pool: pool.ID, 116 | Start: start, 117 | End: end, 118 | } 119 | 120 | subnet, err = ipamClient.CreateShowSubnet(pool.ID, subnet) 121 | Expect(err).To(BeNil()) 122 | Expect(subnet.ID).ToNot(Equal("")) 123 | Expect(subnet.Name).To(Equal("ReservationTestSubnet1")) 124 | Expect(subnet.Pool).To(Equal(pool.ID)) 125 | 126 | }) 127 | 128 | AfterEach(func() { 129 | poolLocation, err := ipamClient.DeletePool(pool.ID, pool) 130 | Expect(err).To(BeNil()) 131 | Expect(poolLocation).To(Equal("/pools")) 132 | }) 133 | 134 | It("Should create a reservation and return that reservation object", func() { 135 | 136 | reservation = resources.ReservationV1{ 137 | Name: "Reservation1", 138 | Subnet: subnet.ID, 139 | } 140 | 141 | reservation, err = ipamClient.CreateShowReservation(subnet.ID, reservation) 142 | Expect(err).To(BeNil()) 143 | Expect(reservation.ID).ToNot(Equal("")) 144 | Expect(reservation.Name).To(Equal("Reservation1")) 145 | Expect(reservation.Subnet).To(Equal(subnet.ID)) 146 | 147 | }) 148 | 149 | }) 150 | var _ = Describe("Leases tests", func() { 151 | var ipamClient *Client 152 | var pool resources.PoolV1 153 | var subnet resources.SubnetV1 154 | var reservation, reservation2 resources.ReservationV1 155 | var leases, leases2 resources.LeasesV1 156 | 157 | BeforeEach(func() { 158 | ipamClient = NewClient(ipamAddress) 159 | Expect(ipamClient).ToNot(BeNil()) 160 | 161 | pool = resources.PoolV1{ 162 | Name: "LeaseTestPool1", 163 | } 164 | 165 | pool, err = ipamClient.CreateShowPool(pool) 166 | Expect(err).To(BeNil()) 167 | Expect(pool.ID).ToNot(Equal("")) 168 | Expect(pool.Name).To(Equal("LeaseTestPool1")) 169 | 170 | subnet = resources.SubnetV1{ 171 | Name: "LeaseTestSubnet1", 172 | Pool: pool.ID, 173 | Start: start, 174 | End: end, 175 | } 176 | 177 | subnet, err = ipamClient.CreateShowSubnet(pool.ID, subnet) 178 | Expect(err).To(BeNil()) 179 | Expect(subnet.ID).ToNot(Equal("")) 180 | Expect(subnet.Name).To(Equal("LeaseTestSubnet1")) 181 | Expect(subnet.Pool).To(Equal(pool.ID)) 182 | 183 | reservation = resources.ReservationV1{ 184 | Name: "LeaseTestReservation1", 185 | Subnet: subnet.ID, 186 | } 187 | 188 | reservation, err = ipamClient.CreateShowReservation(subnet.ID, reservation) 189 | Expect(err).To(BeNil()) 190 | Expect(reservation.ID).ToNot(Equal("")) 191 | Expect(reservation.Name).To(Equal("LeaseTestReservation1")) 192 | Expect(reservation.Subnet).To(Equal(subnet.ID)) 193 | 194 | reservation2 = resources.ReservationV1{ 195 | Name: "LeaseTestReservation2", 196 | Subnet: subnet.ID, 197 | } 198 | 199 | reservation2, err = ipamClient.CreateShowReservation(subnet.ID, reservation2) 200 | Expect(err).To(BeNil()) 201 | Expect(reservation2.ID).ToNot(Equal("")) 202 | Expect(reservation2.Name).To(Equal("LeaseTestReservation2")) 203 | Expect(reservation2.Subnet).To(Equal(subnet.ID)) 204 | 205 | }) 206 | 207 | AfterEach(func() { 208 | poolLocation, err := ipamClient.DeletePool(pool.ID, pool) 209 | Expect(err).To(BeNil()) 210 | Expect(poolLocation).To(Equal("/pools")) 211 | }) 212 | 213 | It("Should show all leases", func() { 214 | leases, err = ipamClient.IndexLeases(reservation.ID) 215 | Expect(err).To(BeNil()) 216 | Expect(leases.Leases[0].ID).ToNot(Equal("")) 217 | Expect(leases.Leases[0].Reservation).To(Equal(reservation.ID)) 218 | 219 | leases2, err = ipamClient.IndexLeases(reservation2.ID) 220 | Expect(err).To(BeNil()) 221 | Expect(leases2.Leases[0].ID).ToNot(Equal("")) 222 | Expect(leases2.Leases[0].Reservation).To(Equal(reservation2.ID)) 223 | 224 | }) 225 | }) 226 | }) 227 | -------------------------------------------------------------------------------- /controllers/helpers/renderers_test.go: -------------------------------------------------------------------------------- 1 | package helpers_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | "net/http" 8 | "strings" 9 | 10 | "gopkg.in/mgo.v2/bson" 11 | 12 | . "github.com/RackHD/ipam/controllers/helpers" 13 | "github.com/RackHD/ipam/models" 14 | "github.com/RackHD/ipam/resources" 15 | 16 | . "github.com/onsi/ginkgo" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | type FakeResponseWriter struct { 21 | headers http.Header 22 | status int 23 | data []byte 24 | } 25 | 26 | func NewFakeResponseWriter(status int, data []byte) FakeResponseWriter { 27 | return FakeResponseWriter{ 28 | headers: http.Header{}, 29 | status: status, 30 | data: data, 31 | } 32 | } 33 | 34 | func (w FakeResponseWriter) Header() http.Header { 35 | return w.headers 36 | } 37 | 38 | func (w FakeResponseWriter) Write(data []byte) (int, error) { 39 | left := strings.TrimSpace(string(data)) 40 | right := strings.TrimSpace(string(w.data)) 41 | Expect(left).To(Equal(right)) 42 | return len(data), nil 43 | } 44 | 45 | func (w FakeResponseWriter) WriteHeader(status int) { 46 | Expect(status).To(Equal(w.status)) 47 | } 48 | 49 | func NewRequest(method, url string, body io.Reader) *http.Request { 50 | req, err := http.NewRequest(method, url, body) 51 | Expect(err).NotTo(HaveOccurred()) 52 | return req 53 | } 54 | 55 | var _ = Describe("Renderers", func() { 56 | Describe("RenderError", func() { 57 | It("should render HTTPStatusError status code and message", func() { 58 | err := NewHTTPStatusError(http.StatusExpectationFailed, "Fake Error") 59 | 60 | w := NewFakeResponseWriter(http.StatusExpectationFailed, ([]byte)(err.Error())) 61 | 62 | RenderError(w, err) 63 | }) 64 | 65 | It("should render default error status code and message", func() { 66 | err := fmt.Errorf("Fake Error") 67 | 68 | w := NewFakeResponseWriter(http.StatusInternalServerError, ([]byte)(err.Error())) 69 | 70 | RenderError(w, err) 71 | }) 72 | }) 73 | 74 | Describe("RenderLocation", func() { 75 | It("should render the given location header, status, and empty body", func() { 76 | w := NewFakeResponseWriter(http.StatusOK, []byte{}) 77 | 78 | RenderLocation(w, http.StatusOK, "/location") 79 | 80 | Expect(w.Header().Get("Location")).To(Equal("/location")) 81 | }) 82 | }) 83 | 84 | Describe("AcceptResource", func() { 85 | It("should render unsupported media type if the mime type was not parsed", func() { 86 | r := NewRequest( 87 | "POST", 88 | "http://fake/pools", 89 | strings.NewReader(`{ 90 | "name": "New Pool", 91 | "tags": ["New Pool Tag"], 92 | "metadata": { 93 | "one": 1 94 | } 95 | }`), 96 | ) 97 | 98 | r.Header.Set(HeaderContentType, ";invalid") 99 | 100 | _, err := AcceptResource(r, resources.PoolResourceType) 101 | 102 | Expect(err.Error()).To(Equal("Invalid Resource: mime: no media type")) 103 | }) 104 | 105 | It("should render unsupported media type if the mime type was not expected", func() { 106 | r := NewRequest( 107 | "POST", 108 | "http://fake/pools", 109 | strings.NewReader(`{ 110 | "name": "New Pool", 111 | "tags": ["New Pool Tag"], 112 | "metadata": { 113 | "one": 1 114 | } 115 | }`), 116 | ) 117 | 118 | r.Header.Set(HeaderContentType, "vnd.ipam.reservation+json;version=1.0.0") 119 | 120 | _, err := AcceptResource(r, resources.PoolResourceType) 121 | 122 | Expect(err.Error()).To(Equal("Unsupported Resource Type: vnd.ipam.reservation != application/vnd.ipam.pool")) 123 | }) 124 | 125 | It("should render unsupported media type if the resource version is not supported", func() { 126 | r := NewRequest( 127 | "POST", 128 | "http://fake/pools", 129 | strings.NewReader(`{ 130 | "name": "New Pool", 131 | "tags": ["New Pool Tag"], 132 | "metadata": { 133 | "one": 1 134 | } 135 | }`), 136 | ) 137 | 138 | r.Header.Set(HeaderContentType, resources.PoolResourceType+";version=0.0.0") 139 | 140 | _, err := AcceptResource(r, resources.PoolResourceType) 141 | 142 | Expect(err.Error()).To(Equal("Unsupported Resource Version: Require: Unable to locate resource application/vnd.ipam.pool, version 0.0.0.")) 143 | }) 144 | 145 | It("should render an error if the body of the request was not valid json", func() { 146 | r := NewRequest( 147 | "POST", 148 | "http://fake/pools", 149 | strings.NewReader(`invalid json`), 150 | ) 151 | 152 | r.Header.Set(HeaderContentType, resources.PoolResourceType+";version=1.0.0") 153 | 154 | _, err := AcceptResource(r, resources.PoolResourceType) 155 | Expect(err.Error()).To(Equal("invalid character 'i' looking for beginning of value")) 156 | }) 157 | 158 | It("should unmarshal a valid resource", func() { 159 | r := NewRequest( 160 | "POST", 161 | "http://fake/pools", 162 | strings.NewReader(`{ 163 | "name": "New Pool", 164 | "tags": ["New Pool Tag"], 165 | "metadata": { 166 | "one": 1 167 | } 168 | }`), 169 | ) 170 | 171 | r.Header.Set(HeaderContentType, resources.PoolResourceType+";version=1.0.0") 172 | 173 | _, err := AcceptResource(r, resources.PoolResourceType) 174 | 175 | Expect(err).ToNot(HaveOccurred()) 176 | }) 177 | }) 178 | 179 | Describe("RenderResource", func() { 180 | var ( 181 | subnet = models.Subnet{ 182 | ID: bson.NewObjectId(), 183 | Name: "Subnet Name", 184 | Tags: []string{"Subnet Tag"}, 185 | Metadata: "Subnet Metadata", 186 | Pool: bson.NewObjectId(), 187 | } 188 | data = []byte{} 189 | ) 190 | 191 | BeforeSuite(func() { 192 | resource := resources.SubnetV1{} 193 | 194 | err := resource.Marshal(subnet) 195 | Expect(err).ToNot(HaveOccurred()) 196 | 197 | data, err = json.Marshal(resource) 198 | Expect(err).ToNot(HaveOccurred()) 199 | }) 200 | 201 | It("should set the media type to the expected type if the media type is absent or incorrect", func() { 202 | r := NewRequest( 203 | "GET", 204 | "http://fake/pools", 205 | nil, 206 | ) 207 | 208 | r.Header.Set(HeaderAccept, "unexpected") 209 | 210 | w := NewFakeResponseWriter(http.StatusOK, data) 211 | 212 | err := RenderResource(w, r, resources.SubnetResourceType, http.StatusOK, subnet) 213 | 214 | Expect(err).ToNot(HaveOccurred()) 215 | }) 216 | 217 | It("should return an error if the requested resource is not present", func() { 218 | r := NewRequest( 219 | "GET", 220 | "http://fake/pools", 221 | nil, 222 | ) 223 | 224 | r.Header.Set(HeaderAccept, resources.SubnetResourceType+";version=1.0.0") 225 | 226 | w := NewFakeResponseWriter(http.StatusOK, data) 227 | 228 | err := RenderResource(w, r, "invalid", http.StatusOK, subnet) 229 | 230 | Expect(err).To(HaveOccurred()) 231 | }) 232 | 233 | It("should set the content type header to the correct resource value", func() { 234 | r := NewRequest( 235 | "GET", 236 | "http://fake/pools", 237 | nil, 238 | ) 239 | 240 | r.Header.Set(HeaderAccept, resources.SubnetResourceType+";version=1.0.0") 241 | 242 | w := NewFakeResponseWriter(http.StatusOK, data) 243 | 244 | err := RenderResource(w, r, resources.SubnetResourceType, http.StatusOK, subnet) 245 | 246 | Expect(err).ToNot(HaveOccurred()) 247 | 248 | Expect(w.Header().Get(HeaderContentType)).To(Equal("application/vnd.ipam.subnet+json; version=1.0.0")) 249 | }) 250 | }) 251 | }) 252 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | # Overview 4 | 5 | IPAM intends to fill a gap around dynamic configuration of resources in datacenter management workflows. A flexible solution providing extensible configuration, reservation, and auditing of IP resources will provide a capability to more easily integrate with different operational designs. 6 | 7 | To that end we are thinking of IPAM as more of a resource allocation service with a strong API & flexible data model while providing less out of the box integration with network services like DHCP & DNS or concerns such as routing topologies. 8 | 9 | Currently IPAM development is driving towards providing the initial technology for it's REST API and underlying data model. As such the plan of record is to implement all of the API end points and their associated data models in a first pass while following up with specific business logic to implement the initial lease allocation and reservation process. The next pass will incorporate extensibility features and flush out Consul integration. 10 | 11 | # Design Overview 12 | 13 | ## Configuration 14 | 15 | Users will be able to create Pools & Subnets through the API. A Pool is a container for one or more Subnets. A Subnet will define a range of IP Addresses that it manages for reservations. Metadata for both objects is a free form field represented as a JSON object to provide storage for Pool/Subnet specific application data. 16 | 17 | ## Reservation 18 | 19 | Users will be able to reserve IP's from either Pools or Subnets. A request against a Subnet will only attempt to reserve addresses in the requested Subnet. A request against a Pool will attempt to reserve addresses in any of the Subnets a Pool manages. 20 | 21 | The Reservation object is created upon request and is used to atomically reserve free addresses in the Subnet(s) requested by updating free addresses with a reference to the reservation object. 22 | A list of obtained Leases is returned along with the reservation object to the requester upon completion. 23 | 24 | ## Inspection 25 | Users can view Pool, Subnet, and Reservation data via the API. Reservations will provide metadata from the Pool & Subnet to which it belongs so that data can be leveraged for additional application specific scenarios. 26 | 27 | ## Extensibility 28 | 29 | The IPAM data model is designed to be extensible allowing consumers to set application specific data on any of the entities provided. IPAM considers this data to be opaque and will provide it upon request to the consumer. 30 | IPAM will also provide an event based notification model over a stateless message bus when configured to do so. Events will be generated for CRUD based events regarding Pools & Subnets as well as reservation events. 31 | Consumers can utilize the events and the available API's to customize their workflows for their application scenarios. 32 | 33 | For example Leases has an update API which is provided to allow consumers to store additional data related to individual allocations based on their application scenario. For instance if an allocated Lease is assigned to a particular entity by MAC address the consumer could place that data into the Lease metadata object for tracking purposes. 34 | 35 | # Getting Started 36 | 37 | ## Prerequisites 38 | 39 | IPAM leverages Docker for it's development & demonstration environments so you'll need to install 40 | the latest Docker (1.12+) to try it out. 41 | 42 | **Mac** 43 | 44 | https://docs.docker.com/docker-for-mac/ 45 | 46 | **Windows 10 Only** 47 | 48 | https://docs.docker.com/docker-for-windows/ 49 | 50 | **Ubuntu** 51 | 52 | https://docs.docker.com/engine/installation/linux/ubuntulinux/ 53 | 54 | In addition IPAM is using make to provide an abstraction for complex Docker commands. On Mac/Linux any version of GNU make is likely suitable. On Windows something like http://gnuwin32.sourceforge.net/packages/make.htm may be suitable. Otherwise the Docker commands can be 55 | run directly using the Makefile as a guide for their format. 56 | 57 | *Note that debug is enabled (by default) in the MongoDB Go driver. If you would like to disable, set "mgoDebug" to false in "main.go", but do so only after you are sure things are working properly. (refer to "Hint" below).* 58 | 59 | 1. git clone git@github.com:RackHD/ipam.git 60 | 2. cd ipam 61 | 3. make 62 | 4. make run 63 | 5. http://localhost:8000/pools 64 | 65 | ## 66 | 67 | **Hint** 68 | 69 | In certain environments, IPAM is unable to connect to MongoDB and will indicate this by logging the following messages in the debug output. 70 | 71 | **ipam | 2016/09/16 19:50:54 no reachable servers** 72 | 73 | **ipam exited with code 1** 74 | 75 | Often times, this is caused by hostname resolution (DNS) problems. Locate the following message in the debug output and check to see if the IP address to which "mongodb" is resolving is correct for your environment. 76 | 77 | **SYNC Address mongodb:27017 resolved as 10.31.61.127:27017** 78 | 79 | If the address is not on one of the Docker "bridge" networks, but rather on some external network, then there is likely an external DNS resolving the "mongodb" hostname. 80 | 81 | To resolve the issue, you must either configure down the NIC that has the external DNS, or reconfigure your environment such that it is not living on a network that has a DNS that will resolve for this hostname. 82 | 83 | ## 84 | 85 | # Details 86 | 87 | ## Planned API End Points 88 | 89 | ### Pool Routes 90 | 91 | * GET /pools 92 | * GET /pools/{id} 93 | * POST /pools 94 | * DELETE /pools/{id} 95 | * PATCH /pools/{id} 96 | 97 | ### Subnet Routes 98 | 99 | * GET /pools/{id}/subnets 100 | * GET /subnets/{id} 101 | * POST /pools/{id}/subnets 102 | * DELETE /subnets/{id} 103 | * PATCH /subnets/{id} 104 | 105 | ### Reservation Routes 106 | 107 | * GET /subnets/{id}/reservations 108 | * GET /reservations/{id} 109 | * POST /subnets/{id}/reservations 110 | * DELETE /reservations/{id} 111 | * PATCH /reservations/{id} 112 | 113 | ### Lease Routes 114 | 115 | * GET /reservations/{id}/leases 116 | * GET /leases/{id} 117 | * PATCH /leases/{id} 118 | 119 | ## Planned Object model 120 | 121 | ### Pool 122 | 123 | Properties 124 | * ID 125 | * Name 126 | * Tags 127 | * Metadata 128 | 129 | Relationships 130 | * Subnets 131 | 132 | ### Subnet 133 | 134 | Properties 135 | * ID 136 | * Name 137 | * Tags 138 | * Metadata 139 | 140 | Relationships 141 | * Pool 142 | 143 | ### Reservation 144 | 145 | Properties 146 | * ID 147 | * Name 148 | * Tags 149 | * Metadata 150 | 151 | Relationships 152 | * Pool/Subnet 153 | * Leases 154 | 155 | ### Lease 156 | 157 | Properties 158 | 159 | * ID 160 | * Address 161 | * Tags 162 | * Metadata 163 | 164 | Relationships 165 | * Reservation 166 | 167 | ## Extensibility Interface 168 | 169 | Initial extensibility will be provided through the tagging and metadata fields on IPAM data models through the API. An event based notification system will be supported via a message bus with initial support for RabbitMQ and/or Nats (http://nats.io). 170 | 171 | As users interact with the API events based on CRUD actions will be sent to the message bus to provide asynchronous feedback to consumers. Further extensibility may be achieved by providing asynchronous request/response patterns to bus consumers for pre-commit hooks such as data validation. 172 | 173 | ## Consul Integration 174 | 175 | IPAM will provide the ability to integrate with Consul for service discovery. Integration will include both identifying a MongoDB service for persistence of the IPAM data model as well as registration of the IPAM service end points. The Consul integration should be considered optional and as such IPAM will offer a command line configuration mechanism suitable for physical and containerized deployments. 176 | 177 | * --mongo = List of comma separated MongoDB servers which will be ignored if the consul flag is present. 178 | * --consul = Connection info for a suitable Consul agent. 179 | * --consul-mongo = Service name for the MongoDB to be utilized by IPAM (defaults to mongodb). 180 | * --consul-service = Service name for IPAM to register itself as with Consul (defaults to ipam). 181 | 182 | ## MVP Deployment Model 183 | 184 | The initial IPAM deployment model will consist of a development/demo environment based on Docker Compose. The composed IPAM application will consist of a single IPAM service in a container paired with a single MongoDB service in a container. Configuration of the container based environment will leverage the MongoDB command line configuration parameter. 185 | 186 | A Consul based deployment (either physical, container, etc) will leverage the Consul command line configuration parameters to instruct IPAM which MongoDB service to locate and what the IPAM service parameters are for service registration with a similar mechanism will be put in place for the message bus extensibility. 187 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /controllers/pools_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | 11 | "github.com/RackHD/ipam/controllers" 12 | "github.com/RackHD/ipam/controllers/helpers" 13 | "github.com/RackHD/ipam/resources" 14 | "github.com/RackHD/ipam/resources/factory" 15 | "github.com/gorilla/mux" 16 | . "github.com/onsi/ginkgo" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var _ = Describe("Pools Controller", func() { 21 | var ( 22 | router *mux.Router 23 | mock *MockIpam 24 | server *httptest.Server 25 | ) 26 | 27 | BeforeEach(func() { 28 | router = mux.NewRouter().StrictSlash(true) 29 | mock = NewMockIpam() 30 | 31 | _, err := controllers.NewPoolsController(router, mock) 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | server = httptest.NewServer(router) 35 | }) 36 | 37 | Describe("Index", func() { 38 | It("should return a 200 status code", func() { 39 | req := NewRequest( 40 | "GET", 41 | server.URL+"/pools", 42 | nil, 43 | ) 44 | 45 | res := Do(req) 46 | 47 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 48 | }) 49 | 50 | It("should return a list of pools", func() { 51 | req := NewRequest( 52 | "GET", 53 | server.URL+"/pools", 54 | nil, 55 | ) 56 | 57 | res := Do(req) 58 | 59 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 60 | 61 | resource, err := factory.Request(resources.PoolsResourceType, "1.0.0") 62 | Expect(err).ToNot(HaveOccurred()) 63 | 64 | err = resource.Marshal(mock.Pools) 65 | Expect(err).ToNot(HaveOccurred()) 66 | 67 | json, err := json.Marshal(resource) 68 | Expect(err).ToNot(HaveOccurred()) 69 | 70 | body, _ := ioutil.ReadAll(res.Body) 71 | Expect(err).ToNot(HaveOccurred()) 72 | 73 | Expect(json).To(Equal(body)) 74 | }) 75 | 76 | It("should return a 500 if an error occurs", func() { 77 | mock.Err = fmt.Errorf("Fail") 78 | 79 | req := NewRequest( 80 | "GET", 81 | server.URL+"/pools", 82 | nil, 83 | ) 84 | 85 | res := Do(req) 86 | 87 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 88 | }) 89 | }) 90 | 91 | Describe("Show", func() { 92 | It("should return a 200 status code", func() { 93 | req := NewRequest( 94 | "GET", 95 | server.URL+"/pools/578af30bbc63780007d99195", 96 | nil, 97 | ) 98 | 99 | res := Do(req) 100 | 101 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 102 | }) 103 | 104 | It("should return the requested pool", func() { 105 | req := NewRequest( 106 | "GET", 107 | server.URL+"/pools/578af30bbc63780007d99195", 108 | nil, 109 | ) 110 | 111 | res := Do(req) 112 | defer res.Body.Close() 113 | 114 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 115 | 116 | resource, err := factory.Request(resources.PoolResourceType, "1.0.0") 117 | Expect(err).ToNot(HaveOccurred()) 118 | 119 | err = resource.Marshal(mock.Pools[0]) 120 | Expect(err).ToNot(HaveOccurred()) 121 | 122 | json, err := json.Marshal(resource) 123 | Expect(err).ToNot(HaveOccurred()) 124 | 125 | body, err := ioutil.ReadAll(res.Body) 126 | Expect(err).ToNot(HaveOccurred()) 127 | 128 | Expect(json).To(Equal(body)) 129 | }) 130 | 131 | It("should return a 500 if an error occurs", func() { 132 | mock.Err = fmt.Errorf("Fail") 133 | 134 | req := NewRequest( 135 | "GET", 136 | server.URL+"/pools/578af30bbc63780007d99195", 137 | nil, 138 | ) 139 | 140 | res := Do(req) 141 | 142 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 143 | }) 144 | 145 | It("should return a 404 if the resource is not found", func() { 146 | mock.Err = fmt.Errorf("not found") 147 | 148 | req := NewRequest( 149 | "GET", 150 | server.URL+"/pools/578af30bbc63780007d99195", 151 | nil, 152 | ) 153 | 154 | res := Do(req) 155 | 156 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 157 | }) 158 | }) 159 | 160 | Describe("Create", func() { 161 | It("should return a 201 status code", func() { 162 | req := NewRequest( 163 | "POST", 164 | server.URL+"/pools", 165 | strings.NewReader(`{ 166 | "name": "New Pool", 167 | "tags": ["New Pool Tag"], 168 | "metadata": { 169 | "one": 1 170 | } 171 | }`), 172 | ) 173 | 174 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 175 | 176 | res := Do(req) 177 | 178 | Expect(res.StatusCode).To(Equal(http.StatusCreated)) 179 | }) 180 | 181 | It("should add the new model with the corresponding fields", func() { 182 | req := NewRequest( 183 | "POST", 184 | server.URL+"/pools", 185 | strings.NewReader(`{ 186 | "name": "New Pool", 187 | "tags": ["New Pool Tag"], 188 | "metadata": { 189 | "one": 1 190 | } 191 | }`), 192 | ) 193 | 194 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 195 | 196 | res := Do(req) 197 | 198 | Expect(res.StatusCode).To(Equal(http.StatusCreated)) 199 | 200 | Expect(mock.PoolCreated).NotTo(BeZero()) 201 | Expect(mock.PoolCreated.Name).To(Equal("New Pool")) 202 | Expect(mock.PoolCreated.Tags).To(Equal([]string{"New Pool Tag"})) 203 | }) 204 | 205 | It("should return a 415 status code if no resource type and version are specified", func() { 206 | req := NewRequest( 207 | "POST", 208 | server.URL+"/pools", 209 | strings.NewReader(`{ 210 | "name": "New Pool", 211 | "tags": ["New Pool Tag"], 212 | "metadata": { 213 | "one": 1 214 | } 215 | }`), 216 | ) 217 | 218 | res := Do(req) 219 | 220 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 221 | }) 222 | 223 | It("should return a 415 status code if the resource version is not available", func() { 224 | req := NewRequest( 225 | "POST", 226 | server.URL+"/pools", 227 | strings.NewReader(`{ 228 | "name": "New Pool", 229 | "tags": ["New Pool Tag"], 230 | "metadata": { 231 | "one": 1 232 | } 233 | }`), 234 | ) 235 | 236 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=0.0.7") 237 | 238 | res := Do(req) 239 | 240 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 241 | }) 242 | 243 | It("should return a 415 status code if the resource specified is the wrong type", func() { 244 | req := NewRequest( 245 | "POST", 246 | server.URL+"/pools", 247 | strings.NewReader(`{ 248 | "name": "New Pool", 249 | "tags": ["New Pool Tag"], 250 | "metadata": { 251 | "one": 1 252 | } 253 | }`), 254 | ) 255 | 256 | req.Header.Set(helpers.HeaderContentType, "application/vnd.ipam.lol;version=1.0.0") 257 | 258 | res := Do(req) 259 | 260 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 261 | }) 262 | 263 | It("should return a 500 if an error occurs", func() { 264 | mock.Err = fmt.Errorf("Fail") 265 | 266 | req := NewRequest( 267 | "POST", 268 | server.URL+"/pools", 269 | strings.NewReader(`{ 270 | "name": "New Pool", 271 | "tags": ["New Pool Tag"], 272 | "metadata": { 273 | "one": 1 274 | } 275 | }`), 276 | ) 277 | 278 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 279 | 280 | res := Do(req) 281 | 282 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 283 | }) 284 | 285 | }) 286 | 287 | Describe("Update", func() { 288 | It("should return a 204 status code", func() { 289 | req := NewRequest( 290 | "PUT", 291 | server.URL+"/pools/578af30bbc63780007d99195", 292 | strings.NewReader(`{ 293 | "name": "Updated Pool", 294 | "tags": ["Updated Pool Tag"], 295 | "metadata": { 296 | "one": "one" 297 | } 298 | }`), 299 | ) 300 | 301 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 302 | 303 | res := Do(req) 304 | 305 | Expect(res.StatusCode).To(Equal(http.StatusNoContent)) 306 | }) 307 | 308 | It("should update the model with the corresponding fields", func() { 309 | req := NewRequest( 310 | "PUT", 311 | server.URL+"/pools/578af30bbc63780007d99195", 312 | strings.NewReader(`{ 313 | "name": "Updated Pool", 314 | "tags": ["Updated Pool Tag"], 315 | "metadata": { 316 | "one": "one" 317 | } 318 | }`), 319 | ) 320 | 321 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 322 | 323 | res := Do(req) 324 | 325 | Expect(res.StatusCode).To(Equal(http.StatusNoContent)) 326 | 327 | Expect(mock.PoolUpdated).NotTo(BeZero()) 328 | Expect(mock.PoolUpdated.Name).To(Equal("Updated Pool")) 329 | Expect(mock.PoolUpdated.Tags).To(Equal([]string{"Updated Pool Tag"})) 330 | }) 331 | 332 | It("should return a 415 status code if no resource type and version are specified", func() { 333 | req := NewRequest( 334 | "PUT", 335 | server.URL+"/pools/578af30bbc63780007d99195", 336 | strings.NewReader(`{ 337 | "name": "Updated Pool", 338 | "tags": ["Updated Pool Tag"], 339 | "metadata": { 340 | "one": "one" 341 | } 342 | }`), 343 | ) 344 | 345 | res := Do(req) 346 | 347 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 348 | }) 349 | 350 | It("should return a 415 status code if the resource version is not available", func() { 351 | req := NewRequest( 352 | "PUT", 353 | server.URL+"/pools/578af30bbc63780007d99195", 354 | strings.NewReader(`{ 355 | "name": "Updated Pool", 356 | "tags": ["Updated Pool Tag"], 357 | "metadata": { 358 | "one": "one" 359 | } 360 | }`), 361 | ) 362 | 363 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=0.0.7") 364 | 365 | res := Do(req) 366 | 367 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 368 | }) 369 | 370 | It("should return a 415 status code if the resource specified is the wrong type", func() { 371 | req := NewRequest( 372 | "PUT", 373 | server.URL+"/pools/578af30bbc63780007d99195", 374 | strings.NewReader(`{ 375 | "name": "Updated Pool", 376 | "tags": ["Updated Pool Tag"], 377 | "metadata": { 378 | "one": "one" 379 | } 380 | }`), 381 | ) 382 | 383 | req.Header.Set(helpers.HeaderContentType, "application/vnd.ipam.lol;version=1.0.0") 384 | 385 | res := Do(req) 386 | 387 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 388 | }) 389 | 390 | It("should return a 500 if an error occurs", func() { 391 | mock.Err = fmt.Errorf("Fail") 392 | 393 | req := NewRequest( 394 | "PUT", 395 | server.URL+"/pools/578af30bbc63780007d99195", 396 | strings.NewReader(`{ 397 | "name": "Updated Pool", 398 | "tags": ["Updated Pool Tag"], 399 | "metadata": { 400 | "one": "one" 401 | } 402 | }`), 403 | ) 404 | 405 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 406 | 407 | res := Do(req) 408 | 409 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 410 | }) 411 | 412 | It("should return a 404 if the resource is not found", func() { 413 | mock.Err = fmt.Errorf("not found") 414 | 415 | req := NewRequest( 416 | "PUT", 417 | server.URL+"/pools/578af30bbc63780007d99195", 418 | strings.NewReader(`{ 419 | "name": "Updated Pool", 420 | "tags": ["Updated Pool Tag"], 421 | "metadata": { 422 | "one": "one" 423 | } 424 | }`), 425 | ) 426 | 427 | req.Header.Set(helpers.HeaderContentType, resources.PoolResourceType+";version=1.0.0") 428 | 429 | res := Do(req) 430 | 431 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 432 | }) 433 | }) 434 | 435 | Describe("Delete", func() { 436 | It("should return a 200 status code", func() { 437 | req := NewRequest( 438 | "DELETE", 439 | server.URL+"/pools/578af30bbc63780007d99195", 440 | nil, 441 | ) 442 | 443 | res := Do(req) 444 | 445 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 446 | }) 447 | 448 | It("should delete the model with the corresponding id", func() { 449 | req := NewRequest( 450 | "DELETE", 451 | server.URL+"/pools/578af30bbc63780007d99195", 452 | nil, 453 | ) 454 | 455 | res := Do(req) 456 | 457 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 458 | 459 | Expect(mock.PoolDeleted).To(Equal("578af30bbc63780007d99195")) 460 | }) 461 | 462 | It("should return a 500 if an error occurs", func() { 463 | mock.Err = fmt.Errorf("Fail") 464 | 465 | req := NewRequest( 466 | "DELETE", 467 | server.URL+"/pools/578af30bbc63780007d99195", 468 | nil, 469 | ) 470 | 471 | res := Do(req) 472 | 473 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 474 | }) 475 | 476 | It("should return a 404 if the resource is not found", func() { 477 | mock.Err = fmt.Errorf("not found") 478 | 479 | req := NewRequest( 480 | "DELETE", 481 | server.URL+"/pools/578af30bbc63780007d99195", 482 | nil, 483 | ) 484 | 485 | res := Do(req) 486 | 487 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 488 | }) 489 | }) 490 | }) 491 | -------------------------------------------------------------------------------- /controllers/subnets_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | 11 | "github.com/RackHD/ipam/controllers" 12 | "github.com/RackHD/ipam/controllers/helpers" 13 | "github.com/RackHD/ipam/resources" 14 | "github.com/RackHD/ipam/resources/factory" 15 | "github.com/gorilla/mux" 16 | . "github.com/onsi/ginkgo" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var _ = Describe("Subnets Controller", func() { 21 | var ( 22 | router *mux.Router 23 | mock *MockIpam 24 | server *httptest.Server 25 | ) 26 | 27 | BeforeEach(func() { 28 | router = mux.NewRouter().StrictSlash(true) 29 | mock = NewMockIpam() 30 | 31 | _, err := controllers.NewSubnetsController(router, mock) 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | server = httptest.NewServer(router) 35 | }) 36 | 37 | Describe("Index", func() { 38 | It("should return a 200 status code", func() { 39 | req := NewRequest( 40 | "GET", 41 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 42 | nil, 43 | ) 44 | 45 | res := Do(req) 46 | 47 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 48 | }) 49 | 50 | It("should return a list of subnets", func() { 51 | req := NewRequest( 52 | "GET", 53 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 54 | nil, 55 | ) 56 | 57 | res := Do(req) 58 | 59 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 60 | 61 | resource, err := factory.Request(resources.SubnetsResourceType, "1.0.0") 62 | Expect(err).ToNot(HaveOccurred()) 63 | 64 | err = resource.Marshal(mock.Subnets) 65 | Expect(err).ToNot(HaveOccurred()) 66 | 67 | json, err := json.Marshal(resource) 68 | Expect(err).ToNot(HaveOccurred()) 69 | 70 | body, _ := ioutil.ReadAll(res.Body) 71 | Expect(err).ToNot(HaveOccurred()) 72 | 73 | Expect(json).To(Equal(body)) 74 | }) 75 | 76 | It("should return a 500 if an error occurs", func() { 77 | mock.Err = fmt.Errorf("Fail") 78 | 79 | req := NewRequest( 80 | "GET", 81 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 82 | nil, 83 | ) 84 | 85 | res := Do(req) 86 | 87 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 88 | }) 89 | }) 90 | 91 | Describe("Show", func() { 92 | It("should return a 200 status code", func() { 93 | req := NewRequest( 94 | "GET", 95 | server.URL+"/subnets/578af30bbc63780007d99195", 96 | nil, 97 | ) 98 | 99 | res := Do(req) 100 | 101 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 102 | }) 103 | 104 | It("should return the requested pool", func() { 105 | req := NewRequest( 106 | "GET", 107 | server.URL+"/subnets/578af30bbc63780007d99195", 108 | nil, 109 | ) 110 | 111 | res := Do(req) 112 | defer res.Body.Close() 113 | 114 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 115 | 116 | resource, err := factory.Request(resources.SubnetResourceType, "1.0.0") 117 | Expect(err).ToNot(HaveOccurred()) 118 | 119 | err = resource.Marshal(mock.Subnets[0]) 120 | Expect(err).ToNot(HaveOccurred()) 121 | 122 | json, err := json.Marshal(resource) 123 | Expect(err).ToNot(HaveOccurred()) 124 | 125 | body, err := ioutil.ReadAll(res.Body) 126 | Expect(err).ToNot(HaveOccurred()) 127 | 128 | Expect(json).To(Equal(body)) 129 | }) 130 | 131 | It("should return a 500 if an error occurs", func() { 132 | mock.Err = fmt.Errorf("Fail") 133 | 134 | req := NewRequest( 135 | "GET", 136 | server.URL+"/subnets/578af30bbc63780007d99195", 137 | nil, 138 | ) 139 | 140 | res := Do(req) 141 | 142 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 143 | }) 144 | 145 | It("should return a 404 if the resource is not found", func() { 146 | mock.Err = fmt.Errorf("not found") 147 | 148 | req := NewRequest( 149 | "GET", 150 | server.URL+"/subnets/578af30bbc63780007d99195", 151 | nil, 152 | ) 153 | 154 | res := Do(req) 155 | 156 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 157 | }) 158 | }) 159 | 160 | Describe("Create", func() { 161 | It("should return a 201 status code", func() { 162 | req := NewRequest( 163 | "POST", 164 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 165 | strings.NewReader(`{ 166 | "name": "New Subnet", 167 | "tags": ["New Subnet Tag"], 168 | "metadata": { 169 | "one": 1 170 | } 171 | }`), 172 | ) 173 | 174 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 175 | 176 | res := Do(req) 177 | 178 | Expect(res.StatusCode).To(Equal(http.StatusCreated)) 179 | }) 180 | 181 | It("should add the new model with the corresponding fields", func() { 182 | req := NewRequest( 183 | "POST", 184 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 185 | strings.NewReader(`{ 186 | "name": "New Subnet", 187 | "tags": ["New Subnet Tag"], 188 | "metadata": { 189 | "one": 1 190 | } 191 | }`), 192 | ) 193 | 194 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 195 | 196 | res := Do(req) 197 | 198 | Expect(res.StatusCode).To(Equal(http.StatusCreated)) 199 | 200 | Expect(mock.SubnetCreated).NotTo(BeZero()) 201 | Expect(mock.SubnetCreated.Name).To(Equal("New Subnet")) 202 | Expect(mock.SubnetCreated.Tags).To(Equal([]string{"New Subnet Tag"})) 203 | }) 204 | 205 | It("should return a 415 status code if no resource type and version are specified", func() { 206 | req := NewRequest( 207 | "POST", 208 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 209 | strings.NewReader(`{ 210 | "name": "New Subnet", 211 | "tags": ["New Subnet Tag"], 212 | "metadata": { 213 | "one": 1 214 | } 215 | }`), 216 | ) 217 | 218 | res := Do(req) 219 | 220 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 221 | }) 222 | 223 | It("should return a 415 status code if the resource version is not available", func() { 224 | req := NewRequest( 225 | "POST", 226 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 227 | strings.NewReader(`{ 228 | "name": "New Subnet", 229 | "tags": ["New Subnet Tag"], 230 | "metadata": { 231 | "one": 1 232 | } 233 | }`), 234 | ) 235 | 236 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=0.0.7") 237 | 238 | res := Do(req) 239 | 240 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 241 | }) 242 | 243 | It("should return a 415 status code if the resource specified is the wrong type", func() { 244 | req := NewRequest( 245 | "POST", 246 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 247 | strings.NewReader(`{ 248 | "name": "New Subnet", 249 | "tags": ["New Subnet Tag"], 250 | "metadata": { 251 | "one": 1 252 | } 253 | }`), 254 | ) 255 | 256 | req.Header.Set(helpers.HeaderContentType, "application/vnd.ipam.lol;version=1.0.0") 257 | 258 | res := Do(req) 259 | 260 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 261 | }) 262 | 263 | It("should return a 500 if an error occurs", func() { 264 | mock.Err = fmt.Errorf("Fail") 265 | 266 | req := NewRequest( 267 | "POST", 268 | server.URL+"/pools/578af30bbc63780007d99195/subnets", 269 | strings.NewReader(`{ 270 | "name": "New Subnet", 271 | "tags": ["New Subnet Tag"], 272 | "metadata": { 273 | "one": 1 274 | } 275 | }`), 276 | ) 277 | 278 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 279 | 280 | res := Do(req) 281 | 282 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 283 | }) 284 | 285 | }) 286 | 287 | Describe("Update", func() { 288 | It("should return a 204 status code", func() { 289 | req := NewRequest( 290 | "PUT", 291 | server.URL+"/subnets/578af30bbc63780007d99195", 292 | strings.NewReader(`{ 293 | "name": "Updated Subnet", 294 | "tags": ["Updated Subnet Tag"], 295 | "metadata": { 296 | "one": "one" 297 | } 298 | }`), 299 | ) 300 | 301 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 302 | 303 | res := Do(req) 304 | 305 | Expect(res.StatusCode).To(Equal(http.StatusNoContent)) 306 | }) 307 | 308 | It("should update the model with the corresponding fields", func() { 309 | req := NewRequest( 310 | "PUT", 311 | server.URL+"/subnets/578af30bbc63780007d99195", 312 | strings.NewReader(`{ 313 | "name": "Updated Subnet", 314 | "tags": ["Updated Subnet Tag"], 315 | "metadata": { 316 | "one": "one" 317 | } 318 | }`), 319 | ) 320 | 321 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 322 | 323 | res := Do(req) 324 | 325 | Expect(res.StatusCode).To(Equal(http.StatusNoContent)) 326 | 327 | Expect(mock.SubnetUpdated).NotTo(BeZero()) 328 | Expect(mock.SubnetUpdated.Name).To(Equal("Updated Subnet")) 329 | Expect(mock.SubnetUpdated.Tags).To(Equal([]string{"Updated Subnet Tag"})) 330 | }) 331 | 332 | It("should return a 415 status code if no resource type and version are specified", func() { 333 | req := NewRequest( 334 | "PUT", 335 | server.URL+"/subnets/578af30bbc63780007d99195", 336 | strings.NewReader(`{ 337 | "name": "Updated Subnet", 338 | "tags": ["Updated Subnet Tag"], 339 | "metadata": { 340 | "one": "one" 341 | } 342 | }`), 343 | ) 344 | 345 | res := Do(req) 346 | 347 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 348 | }) 349 | 350 | It("should return a 415 status code if the resource version is not available", func() { 351 | req := NewRequest( 352 | "PUT", 353 | server.URL+"/subnets/578af30bbc63780007d99195", 354 | strings.NewReader(`{ 355 | "name": "Updated Subnet", 356 | "tags": ["Updated Subnet Tag"], 357 | "metadata": { 358 | "one": "one" 359 | } 360 | }`), 361 | ) 362 | 363 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=0.0.7") 364 | 365 | res := Do(req) 366 | 367 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 368 | }) 369 | 370 | It("should return a 415 status code if the resource specified is the wrong type", func() { 371 | req := NewRequest( 372 | "PUT", 373 | server.URL+"/subnets/578af30bbc63780007d99195", 374 | strings.NewReader(`{ 375 | "name": "Updated Subnet", 376 | "tags": ["Updated Subnet Tag"], 377 | "metadata": { 378 | "one": "one" 379 | } 380 | }`), 381 | ) 382 | 383 | req.Header.Set(helpers.HeaderContentType, "application/vnd.ipam.lol;version=1.0.0") 384 | 385 | res := Do(req) 386 | 387 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 388 | }) 389 | 390 | It("should return a 500 if an error occurs", func() { 391 | mock.Err = fmt.Errorf("Fail") 392 | 393 | req := NewRequest( 394 | "PUT", 395 | server.URL+"/subnets/578af30bbc63780007d99195", 396 | strings.NewReader(`{ 397 | "name": "Updated Subnet", 398 | "tags": ["Updated Subnet Tag"], 399 | "metadata": { 400 | "one": "one" 401 | } 402 | }`), 403 | ) 404 | 405 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 406 | 407 | res := Do(req) 408 | 409 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 410 | }) 411 | 412 | It("should return a 404 if the resource is not found", func() { 413 | mock.Err = fmt.Errorf("not found") 414 | 415 | req := NewRequest( 416 | "PUT", 417 | server.URL+"/subnets/578af30bbc63780007d99195", 418 | strings.NewReader(`{ 419 | "name": "Updated Subnet", 420 | "tags": ["Updated Subnet Tag"], 421 | "metadata": { 422 | "one": "one" 423 | } 424 | }`), 425 | ) 426 | 427 | req.Header.Set(helpers.HeaderContentType, resources.SubnetResourceType+";version=1.0.0") 428 | 429 | res := Do(req) 430 | 431 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 432 | }) 433 | }) 434 | 435 | Describe("Delete", func() { 436 | It("should return a 200 status code", func() { 437 | req := NewRequest( 438 | "DELETE", 439 | server.URL+"/subnets/578af30bbc63780007d99195", 440 | nil, 441 | ) 442 | 443 | res := Do(req) 444 | 445 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 446 | }) 447 | 448 | It("should delete the model with the corresponding id", func() { 449 | req := NewRequest( 450 | "DELETE", 451 | server.URL+"/subnets/578af30bbc63780007d99195", 452 | nil, 453 | ) 454 | 455 | res := Do(req) 456 | 457 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 458 | 459 | Expect(mock.SubnetDeleted).To(Equal("578af30bbc63780007d99195")) 460 | }) 461 | 462 | It("should return a 500 if an error occurs", func() { 463 | mock.Err = fmt.Errorf("Fail") 464 | 465 | req := NewRequest( 466 | "DELETE", 467 | server.URL+"/subnets/578af30bbc63780007d99195", 468 | nil, 469 | ) 470 | 471 | res := Do(req) 472 | 473 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 474 | }) 475 | 476 | It("should return a 404 if the resource is not found", func() { 477 | mock.Err = fmt.Errorf("not found") 478 | 479 | req := NewRequest( 480 | "DELETE", 481 | server.URL+"/subnets/578af30bbc63780007d99195", 482 | nil, 483 | ) 484 | 485 | res := Do(req) 486 | 487 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 488 | }) 489 | }) 490 | }) 491 | -------------------------------------------------------------------------------- /controllers/reservations_test.go: -------------------------------------------------------------------------------- 1 | package controllers_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "net/http/httptest" 9 | "strings" 10 | 11 | "github.com/RackHD/ipam/controllers" 12 | "github.com/RackHD/ipam/controllers/helpers" 13 | "github.com/RackHD/ipam/resources" 14 | "github.com/RackHD/ipam/resources/factory" 15 | "github.com/gorilla/mux" 16 | . "github.com/onsi/ginkgo" 17 | . "github.com/onsi/gomega" 18 | ) 19 | 20 | var _ = Describe("Reservations Controller", func() { 21 | var ( 22 | router *mux.Router 23 | mock *MockIpam 24 | server *httptest.Server 25 | ) 26 | 27 | BeforeEach(func() { 28 | router = mux.NewRouter().StrictSlash(true) 29 | mock = NewMockIpam() 30 | 31 | _, err := controllers.NewReservationsController(router, mock) 32 | Expect(err).NotTo(HaveOccurred()) 33 | 34 | server = httptest.NewServer(router) 35 | }) 36 | 37 | Describe("Index", func() { 38 | It("should return a 200 status code", func() { 39 | req := NewRequest( 40 | "GET", 41 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 42 | nil, 43 | ) 44 | 45 | res := Do(req) 46 | 47 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 48 | }) 49 | 50 | It("should return a list of reservations", func() { 51 | req := NewRequest( 52 | "GET", 53 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 54 | nil, 55 | ) 56 | 57 | res := Do(req) 58 | 59 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 60 | 61 | resource, err := factory.Request(resources.ReservationsResourceType, "1.0.0") 62 | Expect(err).ToNot(HaveOccurred()) 63 | 64 | err = resource.Marshal(mock.Reservations) 65 | Expect(err).ToNot(HaveOccurred()) 66 | 67 | json, err := json.Marshal(resource) 68 | Expect(err).ToNot(HaveOccurred()) 69 | 70 | body, _ := ioutil.ReadAll(res.Body) 71 | Expect(err).ToNot(HaveOccurred()) 72 | 73 | Expect(json).To(Equal(body)) 74 | }) 75 | 76 | It("should return a 500 if an error occurs", func() { 77 | mock.Err = fmt.Errorf("Fail") 78 | 79 | req := NewRequest( 80 | "GET", 81 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 82 | nil, 83 | ) 84 | 85 | res := Do(req) 86 | 87 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 88 | }) 89 | }) 90 | 91 | Describe("Show", func() { 92 | It("should return a 200 status code", func() { 93 | req := NewRequest( 94 | "GET", 95 | server.URL+"/reservations/578af30bbc63780007d99195", 96 | nil, 97 | ) 98 | 99 | res := Do(req) 100 | 101 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 102 | }) 103 | 104 | It("should return the requested subnet", func() { 105 | req := NewRequest( 106 | "GET", 107 | server.URL+"/reservations/578af30bbc63780007d99195", 108 | nil, 109 | ) 110 | 111 | res := Do(req) 112 | defer res.Body.Close() 113 | 114 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 115 | 116 | resource, err := factory.Request(resources.ReservationResourceType, "1.0.0") 117 | Expect(err).ToNot(HaveOccurred()) 118 | 119 | err = resource.Marshal(mock.Reservations[0]) 120 | Expect(err).ToNot(HaveOccurred()) 121 | 122 | json, err := json.Marshal(resource) 123 | Expect(err).ToNot(HaveOccurred()) 124 | 125 | body, err := ioutil.ReadAll(res.Body) 126 | Expect(err).ToNot(HaveOccurred()) 127 | 128 | Expect(json).To(Equal(body)) 129 | }) 130 | 131 | It("should return a 500 if an error occurs", func() { 132 | mock.Err = fmt.Errorf("Fail") 133 | 134 | req := NewRequest( 135 | "GET", 136 | server.URL+"/reservations/578af30bbc63780007d99195", 137 | nil, 138 | ) 139 | 140 | res := Do(req) 141 | 142 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 143 | }) 144 | 145 | It("should return a 404 if the resource is not found", func() { 146 | mock.Err = fmt.Errorf("not found") 147 | 148 | req := NewRequest( 149 | "GET", 150 | server.URL+"/reservations/578af30bbc63780007d99195", 151 | nil, 152 | ) 153 | 154 | res := Do(req) 155 | 156 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 157 | }) 158 | }) 159 | 160 | Describe("Create", func() { 161 | It("should return a 201 status code", func() { 162 | req := NewRequest( 163 | "POST", 164 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 165 | strings.NewReader(`{ 166 | "name": "New Reservation", 167 | "tags": ["New Reservation Tag"], 168 | "metadata": { 169 | "one": 1 170 | } 171 | }`), 172 | ) 173 | 174 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 175 | 176 | res := Do(req) 177 | 178 | Expect(res.StatusCode).To(Equal(http.StatusCreated)) 179 | }) 180 | 181 | It("should add the new model with the corresponding fields", func() { 182 | req := NewRequest( 183 | "POST", 184 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 185 | strings.NewReader(`{ 186 | "name": "New Reservation", 187 | "tags": ["New Reservation Tag"], 188 | "metadata": { 189 | "one": 1 190 | } 191 | }`), 192 | ) 193 | 194 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 195 | 196 | res := Do(req) 197 | 198 | Expect(res.StatusCode).To(Equal(http.StatusCreated)) 199 | 200 | Expect(mock.ReservationCreated).NotTo(BeZero()) 201 | Expect(mock.ReservationCreated.Name).To(Equal("New Reservation")) 202 | Expect(mock.ReservationCreated.Tags).To(Equal([]string{"New Reservation Tag"})) 203 | }) 204 | 205 | It("should return a 415 status code if no resource type and version are specified", func() { 206 | req := NewRequest( 207 | "POST", 208 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 209 | strings.NewReader(`{ 210 | "name": "New Reservation", 211 | "tags": ["New Reservation Tag"], 212 | "metadata": { 213 | "one": 1 214 | } 215 | }`), 216 | ) 217 | 218 | res := Do(req) 219 | 220 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 221 | }) 222 | 223 | It("should return a 415 status code if the resource version is not available", func() { 224 | req := NewRequest( 225 | "POST", 226 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 227 | strings.NewReader(`{ 228 | "name": "New Reservation", 229 | "tags": ["New Reservation Tag"], 230 | "metadata": { 231 | "one": 1 232 | } 233 | }`), 234 | ) 235 | 236 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=0.0.7") 237 | 238 | res := Do(req) 239 | 240 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 241 | }) 242 | 243 | It("should return a 415 status code if the resource specified is the wrong type", func() { 244 | req := NewRequest( 245 | "POST", 246 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 247 | strings.NewReader(`{ 248 | "name": "New Reservation", 249 | "tags": ["New Reservation Tag"], 250 | "metadata": { 251 | "one": 1 252 | } 253 | }`), 254 | ) 255 | 256 | req.Header.Set(helpers.HeaderContentType, "application/vnd.ipam.lol;version=1.0.0") 257 | 258 | res := Do(req) 259 | 260 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 261 | }) 262 | 263 | It("should return a 500 if an error occurs", func() { 264 | mock.Err = fmt.Errorf("Fail") 265 | 266 | req := NewRequest( 267 | "POST", 268 | server.URL+"/subnets/578af30bbc63780007d99195/reservations", 269 | strings.NewReader(`{ 270 | "name": "New Reservation", 271 | "tags": ["New Reservation Tag"], 272 | "metadata": { 273 | "one": 1 274 | } 275 | }`), 276 | ) 277 | 278 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 279 | 280 | res := Do(req) 281 | 282 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 283 | }) 284 | 285 | }) 286 | 287 | Describe("Update", func() { 288 | It("should return a 204 status code", func() { 289 | req := NewRequest( 290 | "PUT", 291 | server.URL+"/reservations/578af30bbc63780007d99195", 292 | strings.NewReader(`{ 293 | "name": "Updated Reservation", 294 | "tags": ["Updated Reservation Tag"], 295 | "metadata": { 296 | "one": "one" 297 | } 298 | }`), 299 | ) 300 | 301 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 302 | 303 | res := Do(req) 304 | 305 | Expect(res.StatusCode).To(Equal(http.StatusNoContent)) 306 | }) 307 | 308 | It("should update the model with the corresponding fields", func() { 309 | req := NewRequest( 310 | "PUT", 311 | server.URL+"/reservations/578af30bbc63780007d99195", 312 | strings.NewReader(`{ 313 | "name": "Updated Reservation", 314 | "tags": ["Updated Reservation Tag"], 315 | "metadata": { 316 | "one": "one" 317 | } 318 | }`), 319 | ) 320 | 321 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 322 | 323 | res := Do(req) 324 | 325 | Expect(res.StatusCode).To(Equal(http.StatusNoContent)) 326 | 327 | Expect(mock.ReservationUpdated).NotTo(BeZero()) 328 | Expect(mock.ReservationUpdated.Name).To(Equal("Updated Reservation")) 329 | Expect(mock.ReservationUpdated.Tags).To(Equal([]string{"Updated Reservation Tag"})) 330 | }) 331 | 332 | It("should return a 415 status code if no resource type and version are specified", func() { 333 | req := NewRequest( 334 | "PUT", 335 | server.URL+"/reservations/578af30bbc63780007d99195", 336 | strings.NewReader(`{ 337 | "name": "Updated Reservation", 338 | "tags": ["Updated Reservation Tag"], 339 | "metadata": { 340 | "one": "one" 341 | } 342 | }`), 343 | ) 344 | 345 | res := Do(req) 346 | 347 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 348 | }) 349 | 350 | It("should return a 415 status code if the resource version is not available", func() { 351 | req := NewRequest( 352 | "PUT", 353 | server.URL+"/reservations/578af30bbc63780007d99195", 354 | strings.NewReader(`{ 355 | "name": "Updated Reservation", 356 | "tags": ["Updated Reservation Tag"], 357 | "metadata": { 358 | "one": "one" 359 | } 360 | }`), 361 | ) 362 | 363 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=0.0.7") 364 | 365 | res := Do(req) 366 | 367 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 368 | }) 369 | 370 | It("should return a 415 status code if the resource specified is the wrong type", func() { 371 | req := NewRequest( 372 | "PUT", 373 | server.URL+"/reservations/578af30bbc63780007d99195", 374 | strings.NewReader(`{ 375 | "name": "Updated Reservation", 376 | "tags": ["Updated Reservation Tag"], 377 | "metadata": { 378 | "one": "one" 379 | } 380 | }`), 381 | ) 382 | 383 | req.Header.Set(helpers.HeaderContentType, "application/vnd.ipam.lol;version=1.0.0") 384 | 385 | res := Do(req) 386 | 387 | Expect(res.StatusCode).To(Equal(http.StatusUnsupportedMediaType)) 388 | }) 389 | 390 | It("should return a 500 if an error occurs", func() { 391 | mock.Err = fmt.Errorf("Fail") 392 | 393 | req := NewRequest( 394 | "PUT", 395 | server.URL+"/reservations/578af30bbc63780007d99195", 396 | strings.NewReader(`{ 397 | "name": "Updated Reservation", 398 | "tags": ["Updated Reservation Tag"], 399 | "metadata": { 400 | "one": "one" 401 | } 402 | }`), 403 | ) 404 | 405 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 406 | 407 | res := Do(req) 408 | 409 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 410 | }) 411 | 412 | It("should return a 404 if the resource is not found", func() { 413 | mock.Err = fmt.Errorf("not found") 414 | 415 | req := NewRequest( 416 | "PUT", 417 | server.URL+"/reservations/578af30bbc63780007d99195", 418 | strings.NewReader(`{ 419 | "name": "Updated Reservation", 420 | "tags": ["Updated Reservation Tag"], 421 | "metadata": { 422 | "one": "one" 423 | } 424 | }`), 425 | ) 426 | 427 | req.Header.Set(helpers.HeaderContentType, resources.ReservationResourceType+";version=1.0.0") 428 | 429 | res := Do(req) 430 | 431 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 432 | }) 433 | }) 434 | 435 | Describe("Delete", func() { 436 | It("should return a 200 status code", func() { 437 | req := NewRequest( 438 | "DELETE", 439 | server.URL+"/reservations/578af30bbc63780007d99195", 440 | nil, 441 | ) 442 | 443 | res := Do(req) 444 | 445 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 446 | }) 447 | 448 | It("should delete the model with the corresponding id", func() { 449 | req := NewRequest( 450 | "DELETE", 451 | server.URL+"/reservations/578af30bbc63780007d99195", 452 | nil, 453 | ) 454 | 455 | res := Do(req) 456 | 457 | Expect(res.StatusCode).To(Equal(http.StatusOK)) 458 | 459 | Expect(mock.ReservationDeleted).To(Equal("578af30bbc63780007d99195")) 460 | }) 461 | 462 | It("should return a 500 if an error occurs", func() { 463 | mock.Err = fmt.Errorf("Fail") 464 | 465 | req := NewRequest( 466 | "DELETE", 467 | server.URL+"/reservations/578af30bbc63780007d99195", 468 | nil, 469 | ) 470 | 471 | res := Do(req) 472 | 473 | Expect(res.StatusCode).To(Equal(http.StatusInternalServerError)) 474 | }) 475 | 476 | It("should return a 404 if the resource is not found", func() { 477 | mock.Err = fmt.Errorf("not found") 478 | 479 | req := NewRequest( 480 | "DELETE", 481 | server.URL+"/reservations/578af30bbc63780007d99195", 482 | nil, 483 | ) 484 | 485 | res := Do(req) 486 | 487 | Expect(res.StatusCode).To(Equal(http.StatusNotFound)) 488 | }) 489 | }) 490 | }) 491 | --------------------------------------------------------------------------------