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

Add, list, read and delete files. #23

Merged
merged 5 commits into from
Apr 6, 2016
Merged
Show file tree
Hide file tree
Changes from 4 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
157 changes: 143 additions & 14 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ package gomaasapi

import (
"encoding/json"
"io"
"io/ioutil"
"net/http"
"net/url"
"path"
"sync/atomic"

"github.com/juju/errors"
Expand Down Expand Up @@ -372,6 +375,111 @@ func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error {
return nil
}

// Files implements Controller.
func (c *controller) Files(prefix string) ([]File, error) {
params := NewURLParams()
params.MaybeAdd("prefix", prefix)
source, err := c.getQuery("files", params.Values)
if err != nil {
return nil, NewUnexpectedError(err)
}
files, err := readFiles(c.apiVersion, source)
if err != nil {
return nil, errors.Trace(err)
}
var result []File
for _, f := range files {
f.controller = c
result = append(result, f)
}
return result, nil
}

// GetFile implements Controller.
func (c *controller) GetFile(filename string) (File, error) {
if filename == "" {
return nil, errors.NotValidf("missing filename")
}
source, err := c.get("files/" + filename)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
if svrErr.StatusCode == http.StatusNotFound {
return nil, errors.Wrap(err, NewNoMatchError(svrErr.BodyMessage))
}
}
return nil, NewUnexpectedError(err)
}
file, err := readFile(c.apiVersion, source)
if err != nil {
return nil, errors.Trace(err)
}
file.controller = c
return file, nil
}

// AddFileArgs is a argument struct for passing information into AddFile.
// One of Content or (Reader, Length) must be specified.
type AddFileArgs struct {
Filename string
Content []byte
Reader io.Reader
Length int64
}

// Validate checks to make sure the filename has no slashes, and that one of
// Content or (Reader, Length) is specified.
func (a *AddFileArgs) Validate() error {
dir, _ := path.Split(a.Filename)
if dir != "" {
return errors.NotValidf("paths in Filename %q", a.Filename)
}
if a.Filename == "" {
return errors.NotValidf("missing Filename")
}
if a.Content == nil {
if a.Reader == nil {
return errors.NotValidf("missing Content or Reader")
}
if a.Length == 0 {
return errors.NotValidf("missing Length")
}
} else {
if a.Reader != nil {
return errors.NotValidf("specifying Content and Reader")
}
if a.Length != 0 {
return errors.NotValidf("specifying Length and Content")
}
}
return nil
}

// AddFile implements Controller.
func (c *controller) AddFile(args AddFileArgs) error {
if err := args.Validate(); err != nil {
return errors.Trace(err)
}
fileContent := args.Content
if fileContent == nil {
content, err := ioutil.ReadAll(io.LimitReader(args.Reader, args.Length))
if err != nil {
return errors.Annotatef(err, "cannot read file content")
}
fileContent = content
}
params := url.Values{"filename": {args.Filename}}
_, err := c.postFile("files", "create", params, fileContent)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
if svrErr.StatusCode == http.StatusBadRequest {
return errors.Wrap(err, NewBadRequestError(svrErr.BodyMessage))
}
}
return NewUnexpectedError(err)
}
return nil
}

func (c *controller) checkCreds() error {
if _, err := c.getOp("users", "whoami"); err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
Expand All @@ -385,16 +493,10 @@ func (c *controller) checkCreds() error {
}

func (c *controller) post(path, op string, params url.Values) (interface{}, error) {
path = EnsureTrailingSlash(path)
requestID := nextRequestID()
logger.Tracef("request %x: POST %s%s?op=%s, params=%s", requestID, c.client.APIURL, path, op, params.Encode())
bytes, err := c.client.Post(&url.URL{Path: path}, op, params, nil)
bytes, err := c._postRaw(path, op, params, nil)
if err != nil {
logger.Tracef("response %x: error: %q", requestID, err.Error())
logger.Tracef("error detail: %#v", err)
return nil, errors.Trace(err)
}
logger.Tracef("response %x: %s", requestID, string(bytes))

var parsed interface{}
err = json.Unmarshal(bytes, &parsed)
Expand All @@ -404,6 +506,26 @@ func (c *controller) post(path, op string, params url.Values) (interface{}, erro
return parsed, nil
}

func (c *controller) postFile(path, op string, params url.Values, fileContent []byte) (interface{}, error) {
// Only one file is ever sent at a time.
files := map[string][]byte{"file": fileContent}
return c._postRaw(path, op, params, files)
}

func (c *controller) _postRaw(path, op string, params url.Values, files map[string][]byte) ([]byte, error) {
path = EnsureTrailingSlash(path)
requestID := nextRequestID()
logger.Tracef("request %x: POST %s%s?op=%s, params=%s", requestID, c.client.APIURL, path, op, params.Encode())
bytes, err := c.client.Post(&url.URL{Path: path}, op, params, files)
if err != nil {
logger.Tracef("response %x: error: %q", requestID, err.Error())
logger.Tracef("error detail: %#v", err)
return nil, errors.Trace(err)
}
logger.Tracef("response %x: %s", requestID, string(bytes))
return bytes, nil
}

func (c *controller) delete(path string) error {
path = EnsureTrailingSlash(path)
requestID := nextRequestID()
Expand Down Expand Up @@ -431,6 +553,19 @@ func (c *controller) getOp(path, op string) (interface{}, error) {
}

func (c *controller) _get(path, op string, params url.Values) (interface{}, error) {
bytes, err := c._getRaw(path, op, params)
if err != nil {
return nil, errors.Trace(err)
}
var parsed interface{}
err = json.Unmarshal(bytes, &parsed)
if err != nil {
return nil, errors.Trace(err)
}
return parsed, nil
}

func (c *controller) _getRaw(path, op string, params url.Values) ([]byte, error) {
path = EnsureTrailingSlash(path)
requestID := nextRequestID()
if logger.IsTraceEnabled() {
Expand All @@ -447,13 +582,7 @@ func (c *controller) _get(path, op string, params url.Values) (interface{}, erro
return nil, errors.Trace(err)
}
logger.Tracef("response %x: %s", requestID, string(bytes))

var parsed interface{}
err = json.Unmarshal(bytes, &parsed)
if err != nil {
return nil, errors.Trace(err)
}
return parsed, nil
return bytes, nil
}

func nextRequestID() int64 {
Expand Down
148 changes: 146 additions & 2 deletions controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@
package gomaasapi

import (
"bytes"
"io/ioutil"
"net/http"
"net/url"

"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/juju/testing"
jc "github.com/juju/testing/checkers"
"github.com/juju/utils/set"
Expand All @@ -27,19 +31,21 @@ func (*versionSuite) TestSupportedVersions(c *gc.C) {
}

type controllerSuite struct {
testing.CleanupSuite
testing.LoggingCleanupSuite
server *SimpleTestServer
}

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

func (s *controllerSuite) SetUpTest(c *gc.C) {
s.CleanupSuite.SetUpTest(c)
s.LoggingCleanupSuite.SetUpTest(c)
loggo.GetLogger("").SetLogLevel(loggo.DEBUG)

server := NewSimpleServer()
server.AddGetResponse("/api/2.0/boot-resources/", http.StatusOK, bootResourcesResponse)
server.AddGetResponse("/api/2.0/devices/", http.StatusOK, devicesResponse)
server.AddGetResponse("/api/2.0/fabrics/", http.StatusOK, fabricResponse)
server.AddGetResponse("/api/2.0/files/", http.StatusOK, filesResponse)
server.AddGetResponse("/api/2.0/machines/", http.StatusOK, machinesResponse)
server.AddGetResponse("/api/2.0/machines/?hostname=untasted-markita", http.StatusOK, "["+machineResponse+"]")
server.AddGetResponse("/api/2.0/spaces/", http.StatusOK, spacesResponse)
Expand Down Expand Up @@ -362,6 +368,144 @@ func (s *controllerSuite) TestReleaseMachinesUnexpected(c *gc.C) {
c.Assert(err.Error(), gc.Equals, "unexpected: ServerError: 502 Bad Gateway (wat)")
}

func (s *controllerSuite) TestFiles(c *gc.C) {
controller := s.getController(c)
files, err := controller.Files("")
c.Assert(err, jc.ErrorIsNil)
c.Assert(files, gc.HasLen, 2)

file := files[0]
c.Assert(file.Filename(), gc.Equals, "test")
uri, err := url.Parse(file.AnonymousURL())
c.Assert(err, jc.ErrorIsNil)

c.Assert(uri.Scheme, gc.Equals, "http")
c.Assert(uri.RequestURI(), gc.Equals, "/MAAS/api/2.0/files/?op=get_by_key&key=3afba564-fb7d-11e5-932f-52540051bf22")
}

func (s *controllerSuite) TestGetFile(c *gc.C) {
s.server.AddGetResponse("/api/2.0/files/testing/", http.StatusOK, fileResponse)
controller := s.getController(c)
file, err := controller.GetFile("testing")
c.Assert(err, jc.ErrorIsNil)

c.Assert(file.Filename(), gc.Equals, "testing")
uri, err := url.Parse(file.AnonymousURL())
c.Assert(err, jc.ErrorIsNil)
c.Assert(uri.Scheme, gc.Equals, "http")
c.Assert(uri.RequestURI(), gc.Equals, "/MAAS/api/2.0/files/?op=get_by_key&key=88e64b76-fb82-11e5-932f-52540051bf22")
}

func (s *controllerSuite) TestGetFileMissing(c *gc.C) {
controller := s.getController(c)
_, err := controller.GetFile("missing")
c.Assert(err, jc.Satisfies, IsNoMatchError)
}

func (s *controllerSuite) TestAddFileArgsValidate(c *gc.C) {
reader := bytes.NewBufferString("test")
for i, test := range []struct {
args AddFileArgs
errText string
}{{
errText: "missing Filename not valid",
}, {
args: AddFileArgs{Filename: "/foo"},
errText: `paths in Filename "/foo" not valid`,
}, {
args: AddFileArgs{Filename: "a/foo"},
errText: `paths in Filename "a/foo" not valid`,
}, {
args: AddFileArgs{Filename: "foo.txt"},
errText: `missing Content or Reader not valid`,
}, {
args: AddFileArgs{
Filename: "foo.txt",
Reader: reader,
},
errText: `missing Length not valid`,
}, {
args: AddFileArgs{
Filename: "foo.txt",
Reader: reader,
Length: 4,
},
}, {
args: AddFileArgs{
Filename: "foo.txt",
Content: []byte("foo"),
Reader: reader,
},
errText: `specifying Content and Reader not valid`,
}, {
args: AddFileArgs{
Filename: "foo.txt",
Content: []byte("foo"),
Length: 20,
},
errText: `specifying Length and Content not valid`,
}, {
args: AddFileArgs{
Filename: "foo.txt",
Content: []byte("foo"),
},
}} {
c.Logf("test %d", i)
err := test.args.Validate()
if test.errText == "" {
c.Check(err, jc.ErrorIsNil)
} else {
c.Check(err, jc.Satisfies, errors.IsNotValid)
c.Check(err.Error(), gc.Equals, test.errText)
}
}
}

func (s *controllerSuite) TestAddFileValidates(c *gc.C) {
controller := s.getController(c)
err := controller.AddFile(AddFileArgs{})
c.Assert(err, jc.Satisfies, errors.IsNotValid)
}

func (s *controllerSuite) assertFile(c *gc.C, request *http.Request, filename, content string) {
form := request.Form
c.Check(form.Get("filename"), gc.Equals, filename)
fileHeader := request.MultipartForm.File["file"][0]
f, err := fileHeader.Open()
c.Assert(err, jc.ErrorIsNil)
bytes, err := ioutil.ReadAll(f)
c.Assert(err, jc.ErrorIsNil)
c.Assert(string(bytes), gc.Equals, content)
}

func (s *controllerSuite) TestAddFileContent(c *gc.C) {
s.server.AddPostResponse("/api/2.0/files/?op=create", http.StatusOK, "")
controller := s.getController(c)
err := controller.AddFile(AddFileArgs{
Filename: "foo.txt",
Content: []byte("foo"),
})
c.Assert(err, jc.ErrorIsNil)

request := s.server.LastRequest()
s.assertFile(c, request, "foo.txt", "foo")
}

func (s *controllerSuite) TestAddFileReader(c *gc.C) {
reader := bytes.NewBufferString("test\n extra over length ignored")
s.server.AddPostResponse("/api/2.0/files/?op=create", http.StatusOK, "")
controller := s.getController(c)
err := controller.AddFile(AddFileArgs{
Filename: "foo.txt",
Reader: reader,
Length: 5,
})
c.Assert(err, jc.ErrorIsNil)

request := s.server.LastRequest()
s.assertFile(c, request, "foo.txt", "test\n")
}

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

type cleanup interface {
Expand Down
Loading