Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create a device #21

Merged
merged 9 commits into from
Apr 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -194,11 +194,50 @@ func (c *controller) Devices(args DevicesArgs) ([]Device, error) {
}
var result []Device
for _, d := range devices {
d.controller = c
result = append(result, d)
}
return result, nil
}

// CreateDeviceArgs is a argument struct for passing information into CreateDevice.
type CreateDeviceArgs struct {
Hostname string
MACAddresses []string
Domain string
Parent string
}

// Devices implements Controller.
func (c *controller) CreateDevice(args CreateDeviceArgs) (Device, error) {
// There must be at least one mac address.
if len(args.MACAddresses) == 0 {
return nil, NewBadRequestError("at least one MAC address must be specified")
}
params := NewURLParams()
params.MaybeAdd("hostname", args.Hostname)
params.MaybeAdd("domain", args.Domain)
params.MaybeAddMany("mac_addresses", args.MACAddresses)
params.MaybeAdd("parent", args.Parent)
result, err := c.post("devices", "create", params.Values)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
if svrErr.StatusCode == http.StatusBadRequest {
return nil, errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
}
}
// Translate http errors.
return nil, NewUnexpectedError(err)
}

device, err := readDevice(c.apiVersion, result)
if err != nil {
return nil, errors.Trace(err)
}
device.controller = c
return device, nil
}

// MachinesArgs is a argument struct for selecting Machines.
// Only machines that match the specified criteria are returned.
type MachinesArgs struct {
Expand Down Expand Up @@ -365,6 +404,20 @@ func (c *controller) post(path, op string, params url.Values) (interface{}, erro
return parsed, nil
}

func (c *controller) delete(path string) error {
path = EnsureTrailingSlash(path)
requestID := nextRequestID()
logger.Tracef("request %x: DELETE %s%s", requestID, c.client.APIURL, path)
err := c.client.Delete(&url.URL{Path: path})
if err != nil {
logger.Tracef("response %x: error: %q", requestID, err.Error())
logger.Tracef("error detail: %#v", err)
return errors.Trace(err)
}
logger.Tracef("response %x: complete", requestID)
return nil
}

func (c *controller) getQuery(path string, params url.Values) (interface{}, error) {
return c._get(path, "", params)
}
Expand Down
67 changes: 67 additions & 0 deletions controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,51 @@ func (s *controllerSuite) TestDevicesArgs(c *gc.C) {
c.Assert(request.URL.Query(), gc.HasLen, 6)
}

func (s *controllerSuite) TestCreateDevice(c *gc.C) {
s.server.AddPostResponse("/api/2.0/devices/?op=create", http.StatusOK, deviceResponse)
controller := s.getController(c)
device, err := controller.CreateDevice(CreateDeviceArgs{
MACAddresses: []string{"a-mac-address"},
})
c.Assert(err, jc.ErrorIsNil)
c.Assert(device.SystemID(), gc.Equals, "4y3ha8")
}

func (s *controllerSuite) TestCreateDeviceMissingAddress(c *gc.C) {
controller := s.getController(c)
_, err := controller.CreateDevice(CreateDeviceArgs{})
c.Assert(err, jc.Satisfies, IsBadRequestError)
c.Assert(err.Error(), gc.Equals, "at least one MAC address must be specified")
}

func (s *controllerSuite) TestCreateDeviceBadRequest(c *gc.C) {
s.server.AddPostResponse("/api/2.0/devices/?op=create", http.StatusBadRequest, "some error")
controller := s.getController(c)
_, err := controller.CreateDevice(CreateDeviceArgs{
MACAddresses: []string{"a-mac-address"},
})
c.Assert(err, jc.Satisfies, IsBadRequestError)
c.Assert(err.Error(), gc.Equals, "some error")
}

func (s *controllerSuite) TestCreateDeviceArgs(c *gc.C) {
s.server.AddPostResponse("/api/2.0/devices/?op=create", http.StatusOK, deviceResponse)
controller := s.getController(c)
// Create an arg structure that sets all the values.
args := CreateDeviceArgs{
Hostname: "foobar",
MACAddresses: []string{"an-address"},
Domain: "a domain",
Parent: "parent",
}
_, err := controller.CreateDevice(args)
c.Assert(err, jc.ErrorIsNil)

request := s.server.LastRequest()
// There should be one entry in the form values for each of the args.
c.Assert(request.PostForm, gc.HasLen, 4)
}

func (s *controllerSuite) TestFabrics(c *gc.C) {
controller := s.getController(c)
fabrics, err := controller.Fabrics()
Expand Down Expand Up @@ -318,3 +363,25 @@ func (s *controllerSuite) TestReleaseMachinesUnexpected(c *gc.C) {
}

var versionResponse = `{"version": "unknown", "subversion": "", "capabilities": ["networks-management", "static-ipaddresses", "ipv6-deployment-ubuntu", "devices-management", "storage-deployment-ubuntu", "network-deployment-ubuntu"]}`

type cleanup interface {
AddCleanup(testing.CleanupFunc)
}

// createTestServerController creates a controller backed on to a test server
// that has sufficient knowledge of versions and users to be able to create a
// valid controller.
func createTestServerController(c *gc.C, suite cleanup) (*SimpleTestServer, Controller) {
server := NewSimpleServer()
server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, `"captain awesome"`)
server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse)
server.Start()
suite.AddCleanup(func(*gc.C) { server.Close() })

controller, err := NewController(ControllerArgs{
BaseURL: server.URL,
APIKey: "fake:as:key",
})
c.Assert(err, jc.ErrorIsNil)
return server, controller
}
47 changes: 45 additions & 2 deletions device.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@
package gomaasapi

import (
"net/http"

"github.com/juju/errors"
"github.com/juju/schema"
"github.com/juju/version"
)

type device struct {
controller *controller

resourceURI string

systemID string
Expand Down Expand Up @@ -45,14 +49,54 @@ func (d *device) Zone() Zone {
return d.zone
}

// Delete implements Device.
func (d *device) Delete() error {
err := d.controller.delete(d.resourceURI)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
switch svrErr.StatusCode {
case http.StatusNotFound:
return errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
case http.StatusForbidden:
return errors.Wrap(err, NewPermissionError(svrErr.BodyMessage))
}
}
return NewUnexpectedError(err)
}
return nil
}

func readDevice(controllerVersion version.Number, source interface{}) (*device, error) {
readFunc, err := getDeviceDeserializationFunc(controllerVersion)
if err != nil {
return nil, errors.Trace(err)
}

checker := schema.StringMap(schema.Any())
coerced, err := checker.Coerce(source, nil)
if err != nil {
return nil, WrapWithDeserializationError(err, "device base schema check failed")
}
valid := coerced.(map[string]interface{})
return readFunc(valid)
}

func readDevices(controllerVersion version.Number, source interface{}) ([]*device, error) {
readFunc, err := getDeviceDeserializationFunc(controllerVersion)
if err != nil {
return nil, errors.Trace(err)
}

checker := schema.List(schema.StringMap(schema.Any()))
coerced, err := checker.Coerce(source, nil)
if err != nil {
return nil, WrapWithDeserializationError(err, "device base schema check failed")
}
valid := coerced.([]interface{})
return readDeviceList(valid, readFunc)
}

func getDeviceDeserializationFunc(controllerVersion version.Number) (deviceDeserializationFunc, error) {
var deserialisationVersion version.Number
for v := range deviceDeserializationFuncs {
if v.Compare(deserialisationVersion) > 0 && v.Compare(controllerVersion) <= 0 {
Expand All @@ -62,8 +106,7 @@ func readDevices(controllerVersion version.Number, source interface{}) ([]*devic
if deserialisationVersion == version.Zero {
return nil, NewUnsupportedVersionError("no device read func for version %s", controllerVersion)
}
readFunc := deviceDeserializationFuncs[deserialisationVersion]
return readDeviceList(valid, readFunc)
return deviceDeserializationFuncs[deserialisationVersion], nil
}

// readDeviceList expects the values of the sourceList to be string maps.
Expand Down
46 changes: 45 additions & 1 deletion device_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,17 @@
package gomaasapi

import (
"net/http"

"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
"github.com/juju/version"
gc "gopkg.in/check.v1"
)

type deviceSuite struct{}
type deviceSuite struct {
testing.CleanupSuite
}

var _ = gc.Suite(&deviceSuite{})

Expand Down Expand Up @@ -45,6 +50,45 @@ func (*deviceSuite) TestHighVersion(c *gc.C) {
c.Assert(devices, gc.HasLen, 1)
}

func (s *deviceSuite) setupDelete(c *gc.C) (*SimpleTestServer, *device) {
server, controller := createTestServerController(c, s)
server.AddGetResponse("/api/2.0/devices/", http.StatusOK, devicesResponse)

devices, err := controller.Devices(DevicesArgs{})
c.Assert(err, jc.ErrorIsNil)
c.Assert(devices, gc.HasLen, 1)
return server, devices[0].(*device)
}

func (s *deviceSuite) TestDelete(c *gc.C) {
server, device := s.setupDelete(c)
// Successful delete is 204 - StatusNoContent
server.AddDeleteResponse(device.resourceURI, http.StatusNoContent, "")
err := device.Delete()
c.Assert(err, jc.ErrorIsNil)
}

func (s *deviceSuite) TestDelete404(c *gc.C) {
_, device := s.setupDelete(c)
// No path, so 404
err := device.Delete()
c.Assert(err, jc.Satisfies, IsNoMatchError)
}

func (s *deviceSuite) TestDeleteForbidden(c *gc.C) {
server, device := s.setupDelete(c)
server.AddDeleteResponse(device.resourceURI, http.StatusForbidden, "")
err := device.Delete()
c.Assert(err, jc.Satisfies, IsPermissionError)
}

func (s *deviceSuite) TestDeleteUnknown(c *gc.C) {
server, device := s.setupDelete(c)
server.AddDeleteResponse(device.resourceURI, http.StatusConflict, "")
err := device.Delete()
c.Assert(err, jc.Satisfies, IsUnexpectedError)
}

const (
deviceResponse = `
{
Expand Down
3 changes: 3 additions & 0 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Controller interface {

// Devices returns a list of devices that match the params.
Devices(DevicesArgs) ([]Device, error)
CreateDevice(CreateDeviceArgs) (Device, error)
}

// Fabric represents a set of interconnected VLANs that are capable of mutual
Expand Down Expand Up @@ -125,6 +126,8 @@ type Device interface {
Zone() Zone

// Parent, Owner, MAC Addresses if needed

Delete() error
}

// Machine represents a physical machine.
Expand Down
14 changes: 1 addition & 13 deletions machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,25 +70,13 @@ func (*machineSuite) TestHighVersion(c *gc.C) {
// Since the start method uses controller pieces, we get the machine from
// the controller.
func (s *machineSuite) startSetup(c *gc.C) (*SimpleTestServer, *machine) {
server := NewSimpleServer()
server, controller := createTestServerController(c, s)
// Just have machines return one machine
server.AddGetResponse("/api/2.0/machines/", http.StatusOK, "["+machineResponse+"]")
server.AddGetResponse("/api/2.0/users/?op=whoami", http.StatusOK, `"captain awesome"`)
server.AddGetResponse("/api/2.0/version/", http.StatusOK, versionResponse)
server.Start()
s.AddCleanup(func(*gc.C) { server.Close() })

controller, err := NewController(ControllerArgs{
BaseURL: server.URL,
APIKey: "fake:as:key",
})
c.Assert(err, jc.ErrorIsNil)

machines, err := controller.Machines(MachinesArgs{})
c.Assert(err, jc.ErrorIsNil)
c.Check(machines, gc.HasLen, 1)
machine := machines[0].(*machine)

return server, machine
}

Expand Down
31 changes: 23 additions & 8 deletions testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -89,20 +89,24 @@ type simpleResponse struct {
type SimpleTestServer struct {
*httptest.Server

getResponses map[string][]simpleResponse
getResponseIndex map[string]int
postResponses map[string][]simpleResponse
postResponseIndex map[string]int
getResponses map[string][]simpleResponse
getResponseIndex map[string]int
postResponses map[string][]simpleResponse
postResponseIndex map[string]int
deleteResponses map[string][]simpleResponse
deleteResponseIndex map[string]int

requests []*http.Request
}

func NewSimpleServer() *SimpleTestServer {
server := &SimpleTestServer{
getResponses: make(map[string][]simpleResponse),
getResponseIndex: make(map[string]int),
postResponses: make(map[string][]simpleResponse),
postResponseIndex: make(map[string]int),
getResponses: make(map[string][]simpleResponse),
getResponseIndex: make(map[string]int),
postResponses: make(map[string][]simpleResponse),
postResponseIndex: make(map[string]int),
deleteResponses: make(map[string][]simpleResponse),
deleteResponseIndex: make(map[string]int),
}
server.Server = httptest.NewUnstartedServer(http.HandlerFunc(server.handler))
return server
Expand All @@ -116,6 +120,10 @@ func (s *SimpleTestServer) AddPostResponse(path string, status int, body string)
s.postResponses[path] = append(s.postResponses[path], simpleResponse{status: status, body: body})
}

func (s *SimpleTestServer) AddDeleteResponse(path string, status int, body string) {
s.deleteResponses[path] = append(s.deleteResponses[path], simpleResponse{status: status, body: body})
}

func (s *SimpleTestServer) LastRequest() *http.Request {
pos := len(s.requests) - 1
if pos < 0 {
Expand Down Expand Up @@ -143,6 +151,13 @@ func (s *SimpleTestServer) handler(writer http.ResponseWriter, request *http.Req
if err != nil {
panic(err)
}
case "DELETE":
responses = s.deleteResponses
responseIndex = s.deleteResponseIndex
_, err := readAndClose(request.Body)
if err != nil {
panic(err)
}
default:
panic("unsupported method " + method)
}
Expand Down