-
Notifications
You must be signed in to change notification settings - Fork 36
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
base: master
Are you sure you want to change the base?
Changes from 13 commits
c8b964c
976de7f
b941989
2040355
6819c3d
37ccf2b
c87b2db
dc8a469
af20fc4
1654585
e41d3af
faf88ac
b9a02ff
9c17aad
7dc2b2b
a26dace
36ad4c4
485aefa
8f19c6c
fce7f0d
565e64b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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++ { | ||
// 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 | ||
} | ||
|
@@ -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 | ||
} | ||
|
@@ -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 | ||
} | ||
|
@@ -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 | ||
} | ||
|
@@ -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 | ||
} | ||
|
@@ -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 | ||
} | ||
|
||
|
@@ -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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 { | ||
|
@@ -378,6 +439,19 @@ func (c *controller) Machines(args MachinesArgs) ([]Machine, error) { | |
return result, nil | ||
} | ||
|
||
func (c *controller) GetMachine(systemID string) (Machine, error) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
@@ -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. | ||
|
@@ -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 { | ||
|
@@ -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 { | ||
|
This file was deleted.
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 | ||
) |
There was a problem hiding this comment.
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?