
Add an end-user client. It maps very closely to swift functionality. Also, rewrite "hummingbird bench" to use said aforementioned client. Change-Id: I48b69254bae375defa16ec46c3c451f52f386bf9
390 lines
11 KiB
Go
390 lines
11 KiB
Go
package client
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// userClient is a Client to be used by end-users. It knows how to authenticate with auth v1 and v2.
|
|
type userClient struct {
|
|
client *http.Client
|
|
ServiceURL string
|
|
AuthToken string
|
|
tenant, username, password, apikey, region, authurl string
|
|
private bool
|
|
}
|
|
|
|
var _ Client = &userClient{}
|
|
|
|
func (c *userClient) authedRequest(method string, path string, body io.Reader, headers map[string]string) (*http.Request, error) {
|
|
req, err := http.NewRequest(method, c.ServiceURL+path, body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("X-Auth-Token", c.AuthToken)
|
|
req.Header.Set("User-Agent", "Hummingbird Client")
|
|
for k, v := range headers {
|
|
req.Header.Set(k, v)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
func (c *userClient) do(req *http.Request) (*http.Response, error) {
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode == 401 {
|
|
resp.Body.Close()
|
|
if c.authenticate() != nil {
|
|
return nil, errors.New("Authentication failed.")
|
|
}
|
|
resp, err = c.client.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
if resp.StatusCode/100 != 2 {
|
|
resp.Body.Close()
|
|
return nil, HTTPError(resp.StatusCode)
|
|
}
|
|
return resp, nil
|
|
}
|
|
|
|
func (c *userClient) doRequest(method string, path string, body io.Reader, headers map[string]string) error {
|
|
req, err := c.authedRequest(method, path, body, headers)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
return nil
|
|
}
|
|
|
|
func (c *userClient) PutAccount(headers map[string]string) (err error) {
|
|
return c.doRequest("PUT", "", nil, headers)
|
|
}
|
|
|
|
func (c *userClient) PostAccount(headers map[string]string) (err error) {
|
|
return c.doRequest("POST", "", nil, headers)
|
|
}
|
|
|
|
func (c *userClient) GetAccount(marker string, endMarker string, limit int, prefix string, delimiter string, headers map[string]string) ([]ContainerRecord, map[string]string, error) {
|
|
limitStr := ""
|
|
if limit > 0 {
|
|
limitStr = strconv.Itoa(limit)
|
|
}
|
|
path := mkquery(map[string]string{"marker": marker, "end_marker": endMarker, "prefix": prefix, "delimiter": delimiter, "limit": limitStr})
|
|
req, err := c.authedRequest("GET", path, nil, headers)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var accountListing []ContainerRecord
|
|
if err := json.Unmarshal(body, &accountListing); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return accountListing, headers2Map(resp.Header), nil
|
|
}
|
|
|
|
func (c *userClient) HeadAccount(headers map[string]string) (map[string]string, error) {
|
|
req, err := c.authedRequest("HEAD", "", nil, headers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Body.Close()
|
|
return headers2Map(resp.Header), nil
|
|
}
|
|
|
|
func (c *userClient) DeleteAccount(headers map[string]string) (err error) {
|
|
return c.doRequest("DELETE", "", nil, nil)
|
|
}
|
|
|
|
func (c *userClient) PutContainer(container string, headers map[string]string) (err error) {
|
|
return c.doRequest("PUT", "/"+container, nil, headers)
|
|
}
|
|
|
|
func (c *userClient) PostContainer(container string, headers map[string]string) (err error) {
|
|
return c.doRequest("POST", "/"+container, nil, headers)
|
|
}
|
|
|
|
func (c *userClient) GetContainer(container string, marker string, endMarker string, limit int, prefix string, delimiter string, headers map[string]string) ([]ObjectRecord, map[string]string, error) {
|
|
limitStr := ""
|
|
if limit > 0 {
|
|
limitStr = strconv.Itoa(limit)
|
|
}
|
|
path := "/" + container + mkquery(map[string]string{"marker": marker, "end_marker": endMarker, "prefix": prefix, "delimiter": delimiter, "limit": limitStr})
|
|
req, err := c.authedRequest("GET", path, nil, headers)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
req.Header.Set("Accept", "application/json")
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
body, err := ioutil.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
var containerListing []ObjectRecord
|
|
if err := json.Unmarshal(body, &containerListing); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return containerListing, headers2Map(resp.Header), nil
|
|
}
|
|
|
|
func (c *userClient) HeadContainer(container string, headers map[string]string) (map[string]string, error) {
|
|
req, err := c.authedRequest("HEAD", "/"+container, nil, headers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Body.Close()
|
|
return headers2Map(resp.Header), nil
|
|
}
|
|
|
|
func (c *userClient) DeleteContainer(container string, headers map[string]string) (err error) {
|
|
return c.doRequest("DELETE", "/"+container, nil, headers)
|
|
}
|
|
|
|
func (c *userClient) PutObject(container string, obj string, headers map[string]string, src io.Reader) (err error) {
|
|
return c.doRequest("PUT", "/"+container+"/"+obj, src, headers)
|
|
}
|
|
|
|
func (c *userClient) PostObject(container string, obj string, headers map[string]string) (err error) {
|
|
return c.doRequest("POST", "/"+container+"/"+obj, nil, headers)
|
|
}
|
|
|
|
func (c *userClient) GetObject(container string, obj string, headers map[string]string) (io.ReadCloser, map[string]string, error) {
|
|
req, err := c.authedRequest("GET", "/"+container+"/"+obj, nil, headers)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
return resp.Body, headers2Map(resp.Header), nil
|
|
}
|
|
|
|
func (c *userClient) HeadObject(container string, obj string, headers map[string]string) (map[string]string, error) {
|
|
req, err := c.authedRequest("HEAD", "/"+container+"/"+obj, nil, headers)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := c.do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resp.Body.Close()
|
|
return headers2Map(resp.Header), nil
|
|
}
|
|
|
|
func (c *userClient) DeleteObject(container string, obj string, headers map[string]string) (err error) {
|
|
return c.doRequest("DELETE", "/"+container+"/"+obj, nil, headers)
|
|
}
|
|
|
|
func (c *userClient) authenticatev1() error {
|
|
req, err := http.NewRequest("GET", c.authurl, nil)
|
|
req.Header.Set("X-Auth-User", c.username)
|
|
req.Header.Set("X-Auth-Key", c.apikey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.client.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
if resp.StatusCode/100 != 2 {
|
|
return HTTPError(resp.StatusCode)
|
|
}
|
|
c.ServiceURL = resp.Header.Get("X-Storage-Url")
|
|
c.AuthToken = resp.Header.Get("X-Auth-Token")
|
|
if c.ServiceURL != "" && c.AuthToken != "" {
|
|
return nil
|
|
}
|
|
return errors.New("Failed to authenticate.")
|
|
}
|
|
|
|
type KeystoneRequestV2 struct {
|
|
Auth interface{} `json:"auth"`
|
|
}
|
|
|
|
type KeystonePasswordAuthV2 struct {
|
|
TenantName string `json:"tenantName"`
|
|
PasswordCredentials struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
} `json:"passwordCredentials"`
|
|
}
|
|
|
|
type RaxAPIKeyAuthV2 struct {
|
|
APIKeyCredentials struct {
|
|
Username string `json:"username"`
|
|
APIKey string `json:"apiKey"`
|
|
} `json:"RAX-KSKEY:apiKeyCredentials"`
|
|
}
|
|
|
|
type KeystoneResponseV2 struct {
|
|
Access struct {
|
|
Token struct {
|
|
ID string `json:"id"`
|
|
Tenant struct {
|
|
Name string `json:"name"`
|
|
ID string `json:"id"`
|
|
} `json:"tenant"`
|
|
} `json:"token"`
|
|
ServiceCatalog []struct {
|
|
Endpoints []struct {
|
|
PublicURL string `json:"publicURL"`
|
|
InternalURL string `json:"internalURL"`
|
|
Region string `json:"region"`
|
|
} `json:"endpoints"`
|
|
Type string `json:"type"`
|
|
} `json:"serviceCatalog"`
|
|
User struct {
|
|
RaxDefaultRegion string `json:"RAX-AUTH:defaultRegion"`
|
|
} `json:"user"`
|
|
} `json:"access"`
|
|
}
|
|
|
|
func (c *userClient) authenticatev2() (err error) {
|
|
if !strings.HasSuffix(c.authurl, "tokens") {
|
|
if c.authurl[len(c.authurl)-1] == '/' {
|
|
c.authurl = c.authurl + "tokens"
|
|
} else {
|
|
c.authurl = c.authurl + "/tokens"
|
|
}
|
|
}
|
|
var authReq []byte
|
|
if c.password != "" {
|
|
creds := &KeystonePasswordAuthV2{TenantName: c.tenant}
|
|
creds.PasswordCredentials.Username = c.username
|
|
creds.PasswordCredentials.Password = c.password
|
|
authReq, err = json.Marshal(&KeystoneRequestV2{Auth: creds})
|
|
} else if c.apikey != "" {
|
|
creds := &RaxAPIKeyAuthV2{}
|
|
creds.APIKeyCredentials.Username = c.username
|
|
creds.APIKeyCredentials.APIKey = c.apikey
|
|
authReq, err = json.Marshal(&KeystoneRequestV2{Auth: creds})
|
|
} else {
|
|
return errors.New("Couldn't figure out what credentials to use.")
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, err := c.client.Post(c.authurl, "application/json", bytes.NewBuffer(authReq))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode/100 != 2 {
|
|
return HTTPError(resp.StatusCode)
|
|
}
|
|
var authResponse KeystoneResponseV2
|
|
if body, err := ioutil.ReadAll(resp.Body); err != nil {
|
|
return err
|
|
} else if err = json.Unmarshal(body, &authResponse); err != nil {
|
|
return err
|
|
}
|
|
c.AuthToken = authResponse.Access.Token.ID
|
|
region := c.region
|
|
if region == "" {
|
|
region = authResponse.Access.User.RaxDefaultRegion
|
|
}
|
|
for _, s := range authResponse.Access.ServiceCatalog {
|
|
if s.Type == "object-store" {
|
|
for _, e := range s.Endpoints {
|
|
if e.Region == region || region == "" || len(s.Endpoints) == 1 {
|
|
if c.private {
|
|
c.ServiceURL = e.InternalURL
|
|
} else {
|
|
c.ServiceURL = e.PublicURL
|
|
}
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return errors.New("Didn't find endpoint")
|
|
}
|
|
|
|
func (c *userClient) authenticate() error {
|
|
if strings.Contains(c.authurl, "/v2") {
|
|
return c.authenticatev2()
|
|
} else {
|
|
return c.authenticatev1()
|
|
}
|
|
}
|
|
|
|
// NewClient creates a new end-user client. It authenticates immediately, and returns an error if unable to.
|
|
func NewClient(tenant string, username string, password string, apikey string, region string, authurl string, private bool) (Client, error) {
|
|
c := &userClient{
|
|
client: &http.Client{Timeout: 30 * time.Minute},
|
|
tenant: tenant,
|
|
username: username,
|
|
password: password,
|
|
apikey: apikey,
|
|
region: region,
|
|
authurl: authurl,
|
|
private: private,
|
|
}
|
|
if err := c.authenticate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// NewInsecureClient creates a new end-user client with SSL verification turned off. It authenticates immediately, and returns an error if unable to.
|
|
func NewInsecureClient(tenant string, username string, password string, apikey string, region string, authurl string, private bool) (Client, error) {
|
|
c := &userClient{
|
|
client: &http.Client{
|
|
Timeout: 30 * time.Minute,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
|
},
|
|
},
|
|
tenant: tenant,
|
|
username: username,
|
|
password: password,
|
|
apikey: apikey,
|
|
region: region,
|
|
authurl: authurl,
|
|
private: private,
|
|
}
|
|
if err := c.authenticate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return c, nil
|
|
}
|