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

Expand API with new Machine, Interface, and Tags interfaces #85

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 13 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
84 changes: 26 additions & 58 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,18 @@ import (
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"

"github.com/hashicorp/go-retryablehttp"
"github.com/juju/errors"
)

const (
// Number of retries performed when the server returns a 503
// response with a 'Retry-after' header. A request will be issued
// at most NumberOfRetries + 1 times.
NumberOfRetries = 4

RetryAfterHeaderName = "Retry-After"
)

// Client represents a way to communicating with a MAAS API instance.
// It is stateless, so it can have concurrent requests in progress.
type Client struct {
APIURL *url.URL
Signer OAuthSigner
APIURL *url.URL
Signer OAuthSigner
httpClient *retryablehttp.Client
}

// ServerError is an http error (or at least, a non-2xx result) received from
Expand Down Expand Up @@ -70,48 +61,14 @@ func readAndClose(stream io.ReadCloser) ([]byte, error) {
// returned error will be ServerError and the returned body will reflect the
// server's response. If the server returns a 503 response with a 'Retry-after'
// header, the request will be transparenty retried.
func (client Client) dispatchRequest(request *http.Request) ([]byte, error) {
// First, store the request's body into a byte[] to be able to restore it
// after each request.
bodyContent, err := readAndClose(request.Body)
if err != nil {
return nil, err
}
for retry := 0; retry < NumberOfRetries; retry++ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, what dies the hashicorp retryable client offer?

// Restore body before issuing request.
newBody := ioutil.NopCloser(bytes.NewReader(bodyContent))
request.Body = newBody
body, err := client.dispatchSingleRequest(request)
// If this is a 503 response with a non-void "Retry-After" header: wait
// as instructed and retry the request.
if err != nil {
serverError, ok := errors.Cause(err).(ServerError)
if ok && serverError.StatusCode == http.StatusServiceUnavailable {
retry_time_int, errConv := strconv.Atoi(serverError.Header.Get(RetryAfterHeaderName))
if errConv == nil {
select {
case <-time.After(time.Duration(retry_time_int) * time.Second):
}
continue
}
}
}
return body, err
}
// Restore body before issuing request.
newBody := ioutil.NopCloser(bytes.NewReader(bodyContent))
request.Body = newBody
return client.dispatchSingleRequest(request)
}
func (client Client) dispatchRequest(request *retryablehttp.Request) ([]byte, error) {
client.Signer.OAuthSign(&request.Header)

func (client Client) dispatchSingleRequest(request *http.Request) ([]byte, error) {
client.Signer.OAuthSign(request)
httpClient := http.Client{}
// See https://code.google.com/p/go/issues/detail?id=4677
// We need to force the connection to close each time so that we don't
// hit the above Go bug.
request.Close = true
response, err := httpClient.Do(request)
response, err := client.httpClient.Do(request)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -148,9 +105,9 @@ func (client Client) Get(uri *url.URL, operation string, parameters url.Values)
if operation != "" {
parameters.Set("op", operation)
}
queryUrl := client.GetURL(uri)
queryUrl.RawQuery = parameters.Encode()
request, err := http.NewRequest("GET", queryUrl.String(), nil)
queryURL := client.GetURL(uri)
queryURL.RawQuery = parameters.Encode()
request, err := retryablehttp.NewRequest("GET", queryURL.String(), nil)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -204,7 +161,7 @@ func (client Client) nonIdempotentRequestFiles(method string, uri *url.URL, para
}
writer.Close()
url := client.GetURL(uri)
request, err := http.NewRequest(method, url.String(), buf)
request, err := retryablehttp.NewRequest(method, url.String(), buf)
if err != nil {
return nil, err
}
Expand All @@ -217,7 +174,7 @@ func (client Client) nonIdempotentRequestFiles(method string, uri *url.URL, para
// requests (but not GET or DELETE requests).
func (client Client) nonIdempotentRequest(method string, uri *url.URL, parameters url.Values) ([]byte, error) {
url := client.GetURL(uri)
request, err := http.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode())))
request, err := retryablehttp.NewRequest(method, url.String(), strings.NewReader(string(parameters.Encode())))
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -245,7 +202,7 @@ func (client Client) Put(uri *url.URL, parameters url.Values) ([]byte, error) {
// Delete deletes an object on the API, using an HTTP "DELETE" request.
func (client Client) Delete(uri *url.URL) error {
url := client.GetURL(uri)
request, err := http.NewRequest("DELETE", url.String(), strings.NewReader(""))
request, err := retryablehttp.NewRequest("DELETE", url.String(), strings.NewReader(""))
if err != nil {
return err
}
Expand All @@ -259,7 +216,7 @@ func (client Client) Delete(uri *url.URL) error {
// Anonymous "signature method" implementation.
type anonSigner struct{}

func (signer anonSigner) OAuthSign(request *http.Request) error {
func (signer anonSigner) OAuthSign(request *http.Header) error {
return nil
}

Expand Down Expand Up @@ -330,5 +287,16 @@ func NewAuthenticatedClient(versionedURL, apiKey string) (*Client, error) {
if err != nil {
return nil, err
}
return &Client{Signer: signer, APIURL: parsedURL}, nil

httpClient := retryablehttp.NewClient()

// Need to re-sign the request before each retry
httpClient.RequestLogHook = func(logger retryablehttp.Logger, request *http.Request, count int) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd need to adapt a github.com/juju/loggo logger as the log writer implementation

err := signer.OAuthSign(&request.Header)
if err != nil {
logger.Printf("[ERROR] Failed to sign request: %v", err)
}
}

return &Client{Signer: signer, APIURL: parsedURL, httpClient: httpClient}, nil
}
146 changes: 144 additions & 2 deletions controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,67 @@ func (c *controller) CreateDevice(args CreateDeviceArgs) (Device, error) {
return device, nil
}

// CreateMachineArgs is a argument struct for passing information into CreateDevice.
type CreateMachineArgs struct {
UpdateMachineArgs
Architecture string
Description string
Commission bool
MACAddresses []string
}

// Validate ensures the arguments are acceptable
func (a *CreateMachineArgs) Validate() error {
if err := a.UpdateMachineArgs.Validate(); err != nil {
return err
}
if len(a.MACAddresses) == 0 {
return fmt.Errorf("at least one MAC address must be specified")
}

return nil
}

// ToParams converts arguments to URL parameters
func (a *CreateMachineArgs) ToParams() *URLParams {
params := a.UpdateMachineArgs.ToParams()
params.MaybeAdd("architecture", a.Architecture)
params.MaybeAdd("description", a.Description)
params.MaybeAddMany("mac_addresses", a.MACAddresses)
if a.Commission {
params.MaybeAdd("commission", "true")
} else {
params.MaybeAdd("commission", "false")
}
return params
}

// CreateMachine implements Controller.
func (c *controller) CreateMachine(args CreateMachineArgs) (Machine, error) {
// There must be at least one mac address.
if err := args.Validate(); err != nil {
return nil, errors.NewBadRequest(err, "Invalid CreateMachine arguments")
}
params := args.ToParams()
result, err := c.post("machines", "", 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)
}

machine, err := readMachine(c.apiVersion, result)
if err != nil {
return nil, errors.Trace(err)
}
machine.controller = c
return machine, 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 @@ -378,6 +439,19 @@ func (c *controller) Machines(args MachinesArgs) ([]Machine, error) {
return result, nil
}

func (c *controller) GetMachine(systemID string) (Machine, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public methods/structs etc should have a comment

source, err := c.getQuery("machines/"+systemID, url.Values{})
if err != nil {
return nil, NewUnexpectedError(err)
}
m, err := readMachine(c.apiVersion, source)
if err != nil {
return nil, errors.Trace(err)
}
m.controller = c
return m, nil
}

func ownerDataMatches(ownerData, filter map[string]string) bool {
for key, value := range filter {
if ownerData[key] != value {
Expand Down Expand Up @@ -617,8 +691,12 @@ func (c *controller) AllocateMachine(args AllocateMachineArgs) (Machine, Constra
// ReleaseMachinesArgs is an argument struct for passing the machine system IDs
// and an optional comment into the ReleaseMachines method.
type ReleaseMachinesArgs struct {
SystemIDs []string
Comment string
SystemIDs []string
Comment string
Erase bool
SecureErase bool
QuickErase bool
Force bool
}

// ReleaseMachines implements Controller.
Expand All @@ -631,6 +709,10 @@ func (c *controller) ReleaseMachines(args ReleaseMachinesArgs) error {
params := NewURLParams()
params.MaybeAddMany("machines", args.SystemIDs)
params.MaybeAdd("comment", args.Comment)
params.MaybeAddBool("erase", args.Erase)
params.MaybeAddBool("secure_erase", args.SecureErase)
params.MaybeAddBool("quick_erase", args.QuickErase)
params.MaybeAddBool("force", args.Force)
_, err := c.post("machines", "release", params.Values)
if err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
Expand Down Expand Up @@ -754,6 +836,66 @@ func (c *controller) AddFile(args AddFileArgs) error {
return nil
}

func (c *controller) Tags() ([]Tag, error) {
source, err := c.getQuery("tags", url.Values{})
if err != nil {
return nil, NewUnexpectedError(err)
}
tags, err := readTags(c.apiVersion, source)
if err != nil {
return nil, errors.Trace(err)
}
var result []Tag
for _, t := range tags {
t.controller = c
result = append(result, t)
}
return result, nil
}

func (c *controller) GetTag(name string) (Tag, error) {
source, err := c.getQuery("tags/"+name, url.Values{})
if err != nil {
return nil, NewUnexpectedError(err)
}
tag, err := readTag(c.apiVersion, source)
if err != nil {
return nil, errors.Trace(err)
}
tag.controller = c
return tag, nil
}

// CreateTagArgs are creation parameters
type CreateTagArgs struct {
Name string
Comment string
Definition string
}

// Validate ensures arguments are valid
func (a *CreateTagArgs) Validate() error {
if a.Name == "" {
return fmt.Errorf("Missing name value")
}
return nil
}

func (c *controller) CreateTag(args CreateTagArgs) (Tag, error) {
if err := args.Validate(); err != nil {
return nil, err
}
params := NewURLParams()
params.MaybeAdd("name", args.Name)
params.MaybeAdd("comment", args.Comment)
params.MaybeAdd("definition", args.Definition)
result, err := c.post("tags", "", params.Values)
if err != nil {
return nil, err
}
return readTag(c.apiVersion, result)
}

func (c *controller) checkCreds() error {
if _, err := c.getOp("users", "whoami"); err != nil {
if svrErr, ok := errors.Cause(err).(ServerError); ok {
Expand Down
13 changes: 0 additions & 13 deletions dependencies.tsv

This file was deleted.

21 changes: 21 additions & 0 deletions enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,25 @@ const (

// The node failed to erase its disks.
NodeStatusFailedDiskErasing = "15"

// The node is in rescue mode.
NodeStatusRescueMode = "16"

// The node is entering rescue mode.
NodeStatusEnteringRescueMode = "17"

// The node failed to enter rescue mode.
NodeStatusFailedEnteringRescueMode = "18"

// The node is exiting rescue mode.
NodeStatusExitingRescueMode = "19"

// The node failed to exit rescue mode.
NodeStatusFailedExitingRescueMode = "20"

// Running tests on Node
NodeStatusTesting = "21"

// Testing has failed
NodeStatusFailedTesting = "22"
)
21 changes: 21 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module github.com/seanhoughton/gomaasapi

go 1.13

require (
github.com/hashicorp/go-retryablehttp v0.6.4
github.com/juju/collections v0.0.0-20180515203731-520e0549d51a
github.com/juju/errors v0.0.0-20150916125642-1b5e39b83d18
github.com/juju/gomaasapi v0.0.0-20190826212825-0ab1eb636aba
github.com/juju/loggo v0.0.0-20170605014607-8232ab8918d9
github.com/juju/retry v0.0.0-20151029024821-62c620325291 // indirect
github.com/juju/schema v0.0.0-20160420044203-075de04f9b7d
github.com/juju/testing v0.0.0-20180402130637-44801989f0f7
github.com/juju/utils v0.0.0-20180424094159-2000ea4ff043 // indirect
github.com/juju/version v0.0.0-20161031051906-1f41e27e54f2
golang.org/x/crypto v0.0.0-20180214000028-650f4a345ab4 // indirect
golang.org/x/net v0.0.0-20180406214816-61147c48b25b // indirect
gopkg.in/check.v1 v1.0.0-20160105164936-4f90aeace3a2
gopkg.in/mgo.v2 v2.0.0-20160818015218-f2b6f6c918c4
gopkg.in/yaml.v2 v2.0.0-20170712054546-1be3d31502d6 // indirect
)
Loading