2016-11-19 20:46:23 +00:00
|
|
|
package goscaleio
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"crypto/tls"
|
|
|
|
"crypto/x509"
|
|
|
|
"encoding/json"
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"net/http"
|
|
|
|
"net/url"
|
|
|
|
"os"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
types "github.com/codedellemc/goscaleio/types/v1"
|
2017-09-05 19:38:57 +00:00
|
|
|
log "github.com/sirupsen/logrus"
|
2016-11-19 20:46:23 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
type Client struct {
|
|
|
|
Token string
|
|
|
|
SIOEndpoint url.URL
|
|
|
|
Http http.Client
|
|
|
|
Insecure string
|
|
|
|
ShowBody bool
|
|
|
|
configConnect *ConfigConnect
|
|
|
|
}
|
|
|
|
|
|
|
|
type Cluster struct {
|
|
|
|
}
|
|
|
|
|
|
|
|
type ConfigConnect struct {
|
|
|
|
Endpoint string
|
|
|
|
Version string
|
|
|
|
Username string
|
|
|
|
Password string
|
|
|
|
}
|
|
|
|
|
|
|
|
type ClientPersistent struct {
|
|
|
|
configConnect *ConfigConnect
|
|
|
|
client *Client
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) getVersion() (string, error) {
|
|
|
|
endpoint := client.SIOEndpoint
|
|
|
|
endpoint.Path = "/api/version"
|
|
|
|
|
|
|
|
req := client.NewRequest(map[string]string{}, "GET", endpoint, nil)
|
|
|
|
req.SetBasicAuth("", client.Token)
|
|
|
|
|
|
|
|
resp, err := client.retryCheckResp(&client.Http, req)
|
|
|
|
if err != nil {
|
|
|
|
return "", fmt.Errorf("problem getting response: %v", err)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
bs, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return "", errors.New("error reading body")
|
|
|
|
}
|
|
|
|
|
|
|
|
version := string(bs)
|
|
|
|
|
|
|
|
if client.ShowBody {
|
|
|
|
log.WithField("body", version).Debug(
|
|
|
|
"printing version message body")
|
|
|
|
}
|
|
|
|
|
|
|
|
version = strings.TrimRight(version, `"`)
|
|
|
|
version = strings.TrimLeft(version, `"`)
|
|
|
|
|
|
|
|
versionRX := regexp.MustCompile(`^(\d+?\.\d+?).*$`)
|
|
|
|
if m := versionRX.FindStringSubmatch(version); len(m) > 0 {
|
|
|
|
return m[1], nil
|
|
|
|
}
|
|
|
|
return version, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) updateVersion() error {
|
|
|
|
|
|
|
|
version, err := client.getVersion()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
client.configConnect.Version = version
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) Authenticate(configConnect *ConfigConnect) (Cluster, error) {
|
|
|
|
|
|
|
|
configConnect.Version = client.configConnect.Version
|
|
|
|
client.configConnect = configConnect
|
|
|
|
|
|
|
|
endpoint := client.SIOEndpoint
|
|
|
|
endpoint.Path += "/login"
|
|
|
|
|
|
|
|
req := client.NewRequest(map[string]string{}, "GET", endpoint, nil)
|
|
|
|
req.SetBasicAuth(configConnect.Username, configConnect.Password)
|
|
|
|
|
|
|
|
httpClient := &client.Http
|
|
|
|
resp, errBody, err := client.checkResp(httpClient.Do(req))
|
|
|
|
if errBody == nil && err != nil {
|
|
|
|
return Cluster{}, err
|
|
|
|
} else if errBody != nil && err != nil {
|
|
|
|
if resp == nil {
|
|
|
|
return Cluster{}, errors.New("Problem getting response from endpoint")
|
|
|
|
}
|
|
|
|
return Cluster{}, errors.New(errBody.Message)
|
|
|
|
}
|
|
|
|
defer resp.Body.Close()
|
|
|
|
|
|
|
|
bs, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return Cluster{}, errors.New("error reading body")
|
|
|
|
}
|
|
|
|
|
|
|
|
token := string(bs)
|
|
|
|
|
|
|
|
if client.ShowBody {
|
|
|
|
log.WithField("body", token).Debug(
|
|
|
|
"printing authentication message body")
|
|
|
|
}
|
|
|
|
|
|
|
|
token = strings.TrimRight(token, `"`)
|
|
|
|
token = strings.TrimLeft(token, `"`)
|
|
|
|
client.Token = token
|
|
|
|
|
|
|
|
if client.configConnect.Version == "" {
|
|
|
|
err = client.updateVersion()
|
|
|
|
if err != nil {
|
|
|
|
return Cluster{}, errors.New("error getting version of ScaleIO")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return Cluster{}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
//https://github.com/chrislusf/teeproxy/blob/master/teeproxy.go
|
|
|
|
type nopCloser struct {
|
|
|
|
io.Reader
|
|
|
|
}
|
|
|
|
|
|
|
|
func (nopCloser) Close() error { return nil }
|
|
|
|
|
|
|
|
func DuplicateRequest(request *http.Request) (request1 *http.Request, request2 *http.Request) {
|
|
|
|
request1 = &http.Request{
|
|
|
|
Method: request.Method,
|
|
|
|
URL: request.URL,
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: request.Header,
|
|
|
|
Host: request.Host,
|
|
|
|
ContentLength: request.ContentLength,
|
|
|
|
}
|
|
|
|
request2 = &http.Request{
|
|
|
|
Method: request.Method,
|
|
|
|
URL: request.URL,
|
|
|
|
Proto: "HTTP/1.1",
|
|
|
|
ProtoMajor: 1,
|
|
|
|
ProtoMinor: 1,
|
|
|
|
Header: request.Header,
|
|
|
|
Host: request.Host,
|
|
|
|
ContentLength: request.ContentLength,
|
|
|
|
}
|
|
|
|
|
|
|
|
if request.Body != nil {
|
|
|
|
b1 := new(bytes.Buffer)
|
|
|
|
b2 := new(bytes.Buffer)
|
|
|
|
w := io.MultiWriter(b1, b2)
|
|
|
|
io.Copy(w, request.Body)
|
|
|
|
request1.Body = nopCloser{b1}
|
|
|
|
request2.Body = nopCloser{b2}
|
|
|
|
|
|
|
|
defer request.Body.Close()
|
|
|
|
}
|
|
|
|
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) retryCheckResp(httpClient *http.Client, req *http.Request) (*http.Response, error) {
|
|
|
|
|
|
|
|
req1, req2 := DuplicateRequest(req)
|
|
|
|
resp, errBody, err := client.checkResp(httpClient.Do(req1))
|
|
|
|
if errBody == nil && err != nil {
|
|
|
|
return &http.Response{}, err
|
|
|
|
} else if errBody != nil && err != nil {
|
|
|
|
if resp == nil {
|
|
|
|
return nil, errors.New("Problem getting response from endpoint")
|
|
|
|
}
|
|
|
|
|
|
|
|
if resp.StatusCode == 401 && errBody.MajorErrorCode == 0 {
|
|
|
|
_, err := client.Authenticate(client.configConnect)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Error re-authenticating: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
ioutil.ReadAll(resp.Body)
|
|
|
|
resp.Body.Close()
|
|
|
|
|
|
|
|
req2.SetBasicAuth("", client.Token)
|
|
|
|
resp, errBody, err = client.checkResp(httpClient.Do(req2))
|
|
|
|
if err != nil {
|
|
|
|
return &http.Response{}, errors.New(errBody.Message)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return &http.Response{}, errors.New(errBody.Message)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return resp, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) checkResp(resp *http.Response, err error) (*http.Response, *types.Error, error) {
|
|
|
|
if err != nil {
|
|
|
|
return resp, &types.Error{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
switch i := resp.StatusCode; {
|
|
|
|
// Valid request, return the response.
|
|
|
|
case i == 200 || i == 201 || i == 202 || i == 204:
|
|
|
|
return resp, &types.Error{}, nil
|
|
|
|
// Invalid request, parse the XML error returned and return it.
|
|
|
|
case i == 400 || i == 401 || i == 403 || i == 404 || i == 405 || i == 406 || i == 409 || i == 415 || i == 500 || i == 503 || i == 504:
|
|
|
|
errBody, err := client.parseErr(resp)
|
|
|
|
return resp, errBody, err
|
|
|
|
// Unhandled response.
|
|
|
|
default:
|
|
|
|
return nil, &types.Error{}, fmt.Errorf("unhandled API response, please report this issue, status code: %s", resp.Status)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) decodeBody(resp *http.Response, out interface{}) error {
|
|
|
|
|
|
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if client.ShowBody {
|
|
|
|
var prettyJSON bytes.Buffer
|
|
|
|
_ = json.Indent(&prettyJSON, body, "", " ")
|
|
|
|
log.WithField("body", prettyJSON.String()).Debug(
|
|
|
|
"print decoded body")
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = json.Unmarshal(body, &out); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (client *Client) parseErr(resp *http.Response) (*types.Error, error) {
|
|
|
|
|
|
|
|
errBody := new(types.Error)
|
|
|
|
|
|
|
|
// if there was an error decoding the body, just return that
|
|
|
|
if err := client.decodeBody(resp, errBody); err != nil {
|
|
|
|
return &types.Error{}, fmt.Errorf("error parsing error body for non-200 request: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return errBody, fmt.Errorf("API (%d) Error: %d: %s", resp.StatusCode, errBody.MajorErrorCode, errBody.Message)
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *Client) NewRequest(params map[string]string, method string, u url.URL, body io.Reader) *http.Request {
|
|
|
|
|
|
|
|
if log.GetLevel() == log.DebugLevel && c.ShowBody && body != nil {
|
|
|
|
buf := new(bytes.Buffer)
|
|
|
|
buf.ReadFrom(body)
|
|
|
|
log.WithField("body", buf.String()).Debug("print new request body")
|
|
|
|
}
|
|
|
|
|
|
|
|
p := url.Values{}
|
|
|
|
|
|
|
|
for k, v := range params {
|
|
|
|
p.Add(k, v)
|
|
|
|
}
|
|
|
|
|
|
|
|
u.RawQuery = p.Encode()
|
|
|
|
|
|
|
|
req, _ := http.NewRequest(method, u.String(), body)
|
|
|
|
|
|
|
|
return req
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewClient() (client *Client, err error) {
|
|
|
|
return NewClientWithArgs(
|
|
|
|
os.Getenv("GOSCALEIO_ENDPOINT"),
|
|
|
|
os.Getenv("GOSCALEIO_VERSION"),
|
|
|
|
os.Getenv("GOSCALEIO_INSECURE") == "true",
|
|
|
|
os.Getenv("GOSCALEIO_USECERTS") == "true")
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewClientWithArgs(
|
|
|
|
endpoint string,
|
|
|
|
version string,
|
|
|
|
insecure,
|
|
|
|
useCerts bool) (client *Client, err error) {
|
|
|
|
|
|
|
|
fields := map[string]interface{}{
|
|
|
|
"endpoint": endpoint,
|
|
|
|
"insecure": insecure,
|
|
|
|
"useCerts": useCerts,
|
|
|
|
"version": version,
|
|
|
|
}
|
|
|
|
|
|
|
|
var uri *url.URL
|
|
|
|
|
|
|
|
if endpoint != "" {
|
|
|
|
uri, err = url.ParseRequestURI(endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return &Client{},
|
|
|
|
withFieldsE(fields, "error parsing endpoint", err)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
return &Client{},
|
|
|
|
withFields(fields, "endpoint is required")
|
|
|
|
}
|
|
|
|
|
|
|
|
client = &Client{
|
|
|
|
SIOEndpoint: *uri,
|
|
|
|
Http: http.Client{
|
|
|
|
Transport: &http.Transport{
|
|
|
|
TLSHandshakeTimeout: 120 * time.Second,
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
InsecureSkipVerify: insecure,
|
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
if useCerts {
|
|
|
|
pool := x509.NewCertPool()
|
|
|
|
pool.AppendCertsFromPEM(pemCerts)
|
|
|
|
|
|
|
|
client.Http.Transport = &http.Transport{
|
|
|
|
TLSHandshakeTimeout: 120 * time.Second,
|
|
|
|
TLSClientConfig: &tls.Config{
|
|
|
|
RootCAs: pool,
|
|
|
|
InsecureSkipVerify: insecure,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
client.configConnect = &ConfigConnect{
|
|
|
|
Version: version,
|
|
|
|
}
|
|
|
|
|
|
|
|
return client, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func GetLink(links []*types.Link, rel string) (*types.Link, error) {
|
|
|
|
for _, link := range links {
|
|
|
|
if link.Rel == rel {
|
|
|
|
return link, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &types.Link{}, errors.New("Couldn't find link")
|
|
|
|
}
|
|
|
|
|
|
|
|
func withFields(fields map[string]interface{}, message string) error {
|
|
|
|
return withFieldsE(fields, message, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
func withFieldsE(
|
|
|
|
fields map[string]interface{}, message string, inner error) error {
|
|
|
|
|
|
|
|
if fields == nil {
|
|
|
|
fields = make(map[string]interface{})
|
|
|
|
}
|
|
|
|
|
|
|
|
if inner != nil {
|
|
|
|
fields["inner"] = inner
|
|
|
|
}
|
|
|
|
|
|
|
|
x := 0
|
|
|
|
l := len(fields)
|
|
|
|
|
|
|
|
var b bytes.Buffer
|
|
|
|
for k, v := range fields {
|
|
|
|
if x < l-1 {
|
|
|
|
b.WriteString(fmt.Sprintf("%s=%v,", k, v))
|
|
|
|
} else {
|
|
|
|
b.WriteString(fmt.Sprintf("%s=%v", k, v))
|
|
|
|
}
|
|
|
|
x = x + 1
|
|
|
|
}
|
|
|
|
|
|
|
|
return newf("%s %s", message, b.String())
|
|
|
|
}
|
|
|
|
|
|
|
|
func newf(format string, a ...interface{}) error {
|
|
|
|
return errors.New(fmt.Sprintf(format, a))
|
|
|
|
}
|