api.go 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402
  1. package goscaleio
  2. import (
  3. "bytes"
  4. "crypto/tls"
  5. "crypto/x509"
  6. "encoding/json"
  7. "errors"
  8. "fmt"
  9. "io"
  10. "io/ioutil"
  11. "net/http"
  12. "net/url"
  13. "os"
  14. "regexp"
  15. "strings"
  16. "time"
  17. types "github.com/codedellemc/goscaleio/types/v1"
  18. log "github.com/sirupsen/logrus"
  19. )
  20. type Client struct {
  21. Token string
  22. SIOEndpoint url.URL
  23. Http http.Client
  24. Insecure string
  25. ShowBody bool
  26. configConnect *ConfigConnect
  27. }
  28. type Cluster struct {
  29. }
  30. type ConfigConnect struct {
  31. Endpoint string
  32. Version string
  33. Username string
  34. Password string
  35. }
  36. type ClientPersistent struct {
  37. configConnect *ConfigConnect
  38. client *Client
  39. }
  40. func (client *Client) getVersion() (string, error) {
  41. endpoint := client.SIOEndpoint
  42. endpoint.Path = "/api/version"
  43. req := client.NewRequest(map[string]string{}, "GET", endpoint, nil)
  44. req.SetBasicAuth("", client.Token)
  45. resp, err := client.retryCheckResp(&client.Http, req)
  46. if err != nil {
  47. return "", fmt.Errorf("problem getting response: %v", err)
  48. }
  49. defer resp.Body.Close()
  50. bs, err := ioutil.ReadAll(resp.Body)
  51. if err != nil {
  52. return "", errors.New("error reading body")
  53. }
  54. version := string(bs)
  55. if client.ShowBody {
  56. log.WithField("body", version).Debug(
  57. "printing version message body")
  58. }
  59. version = strings.TrimRight(version, `"`)
  60. version = strings.TrimLeft(version, `"`)
  61. versionRX := regexp.MustCompile(`^(\d+?\.\d+?).*$`)
  62. if m := versionRX.FindStringSubmatch(version); len(m) > 0 {
  63. return m[1], nil
  64. }
  65. return version, nil
  66. }
  67. func (client *Client) updateVersion() error {
  68. version, err := client.getVersion()
  69. if err != nil {
  70. return err
  71. }
  72. client.configConnect.Version = version
  73. return nil
  74. }
  75. func (client *Client) Authenticate(configConnect *ConfigConnect) (Cluster, error) {
  76. configConnect.Version = client.configConnect.Version
  77. client.configConnect = configConnect
  78. endpoint := client.SIOEndpoint
  79. endpoint.Path += "/login"
  80. req := client.NewRequest(map[string]string{}, "GET", endpoint, nil)
  81. req.SetBasicAuth(configConnect.Username, configConnect.Password)
  82. httpClient := &client.Http
  83. resp, errBody, err := client.checkResp(httpClient.Do(req))
  84. if errBody == nil && err != nil {
  85. return Cluster{}, err
  86. } else if errBody != nil && err != nil {
  87. if resp == nil {
  88. return Cluster{}, errors.New("Problem getting response from endpoint")
  89. }
  90. return Cluster{}, errors.New(errBody.Message)
  91. }
  92. defer resp.Body.Close()
  93. bs, err := ioutil.ReadAll(resp.Body)
  94. if err != nil {
  95. return Cluster{}, errors.New("error reading body")
  96. }
  97. token := string(bs)
  98. if client.ShowBody {
  99. log.WithField("body", token).Debug(
  100. "printing authentication message body")
  101. }
  102. token = strings.TrimRight(token, `"`)
  103. token = strings.TrimLeft(token, `"`)
  104. client.Token = token
  105. if client.configConnect.Version == "" {
  106. err = client.updateVersion()
  107. if err != nil {
  108. return Cluster{}, errors.New("error getting version of ScaleIO")
  109. }
  110. }
  111. return Cluster{}, nil
  112. }
  113. //https://github.com/chrislusf/teeproxy/blob/master/teeproxy.go
  114. type nopCloser struct {
  115. io.Reader
  116. }
  117. func (nopCloser) Close() error { return nil }
  118. func DuplicateRequest(request *http.Request) (request1 *http.Request, request2 *http.Request) {
  119. request1 = &http.Request{
  120. Method: request.Method,
  121. URL: request.URL,
  122. Proto: "HTTP/1.1",
  123. ProtoMajor: 1,
  124. ProtoMinor: 1,
  125. Header: request.Header,
  126. Host: request.Host,
  127. ContentLength: request.ContentLength,
  128. }
  129. request2 = &http.Request{
  130. Method: request.Method,
  131. URL: request.URL,
  132. Proto: "HTTP/1.1",
  133. ProtoMajor: 1,
  134. ProtoMinor: 1,
  135. Header: request.Header,
  136. Host: request.Host,
  137. ContentLength: request.ContentLength,
  138. }
  139. if request.Body != nil {
  140. b1 := new(bytes.Buffer)
  141. b2 := new(bytes.Buffer)
  142. w := io.MultiWriter(b1, b2)
  143. io.Copy(w, request.Body)
  144. request1.Body = nopCloser{b1}
  145. request2.Body = nopCloser{b2}
  146. defer request.Body.Close()
  147. }
  148. return
  149. }
  150. func (client *Client) retryCheckResp(httpClient *http.Client, req *http.Request) (*http.Response, error) {
  151. req1, req2 := DuplicateRequest(req)
  152. resp, errBody, err := client.checkResp(httpClient.Do(req1))
  153. if errBody == nil && err != nil {
  154. return &http.Response{}, err
  155. } else if errBody != nil && err != nil {
  156. if resp == nil {
  157. return nil, errors.New("Problem getting response from endpoint")
  158. }
  159. if resp.StatusCode == 401 && errBody.MajorErrorCode == 0 {
  160. _, err := client.Authenticate(client.configConnect)
  161. if err != nil {
  162. return nil, fmt.Errorf("Error re-authenticating: %s", err)
  163. }
  164. ioutil.ReadAll(resp.Body)
  165. resp.Body.Close()
  166. req2.SetBasicAuth("", client.Token)
  167. resp, errBody, err = client.checkResp(httpClient.Do(req2))
  168. if err != nil {
  169. return &http.Response{}, errors.New(errBody.Message)
  170. }
  171. } else {
  172. return &http.Response{}, errors.New(errBody.Message)
  173. }
  174. }
  175. return resp, nil
  176. }
  177. func (client *Client) checkResp(resp *http.Response, err error) (*http.Response, *types.Error, error) {
  178. if err != nil {
  179. return resp, &types.Error{}, err
  180. }
  181. switch i := resp.StatusCode; {
  182. // Valid request, return the response.
  183. case i == 200 || i == 201 || i == 202 || i == 204:
  184. return resp, &types.Error{}, nil
  185. // Invalid request, parse the XML error returned and return it.
  186. case i == 400 || i == 401 || i == 403 || i == 404 || i == 405 || i == 406 || i == 409 || i == 415 || i == 500 || i == 503 || i == 504:
  187. errBody, err := client.parseErr(resp)
  188. return resp, errBody, err
  189. // Unhandled response.
  190. default:
  191. return nil, &types.Error{}, fmt.Errorf("unhandled API response, please report this issue, status code: %s", resp.Status)
  192. }
  193. }
  194. func (client *Client) decodeBody(resp *http.Response, out interface{}) error {
  195. body, err := ioutil.ReadAll(resp.Body)
  196. if err != nil {
  197. return err
  198. }
  199. if client.ShowBody {
  200. var prettyJSON bytes.Buffer
  201. _ = json.Indent(&prettyJSON, body, "", " ")
  202. log.WithField("body", prettyJSON.String()).Debug(
  203. "print decoded body")
  204. }
  205. if err = json.Unmarshal(body, &out); err != nil {
  206. return err
  207. }
  208. return nil
  209. }
  210. func (client *Client) parseErr(resp *http.Response) (*types.Error, error) {
  211. errBody := new(types.Error)
  212. // if there was an error decoding the body, just return that
  213. if err := client.decodeBody(resp, errBody); err != nil {
  214. return &types.Error{}, fmt.Errorf("error parsing error body for non-200 request: %s", err)
  215. }
  216. return errBody, fmt.Errorf("API (%d) Error: %d: %s", resp.StatusCode, errBody.MajorErrorCode, errBody.Message)
  217. }
  218. func (c *Client) NewRequest(params map[string]string, method string, u url.URL, body io.Reader) *http.Request {
  219. if log.GetLevel() == log.DebugLevel && c.ShowBody && body != nil {
  220. buf := new(bytes.Buffer)
  221. buf.ReadFrom(body)
  222. log.WithField("body", buf.String()).Debug("print new request body")
  223. }
  224. p := url.Values{}
  225. for k, v := range params {
  226. p.Add(k, v)
  227. }
  228. u.RawQuery = p.Encode()
  229. req, _ := http.NewRequest(method, u.String(), body)
  230. return req
  231. }
  232. func NewClient() (client *Client, err error) {
  233. return NewClientWithArgs(
  234. os.Getenv("GOSCALEIO_ENDPOINT"),
  235. os.Getenv("GOSCALEIO_VERSION"),
  236. os.Getenv("GOSCALEIO_INSECURE") == "true",
  237. os.Getenv("GOSCALEIO_USECERTS") == "true")
  238. }
  239. func NewClientWithArgs(
  240. endpoint string,
  241. version string,
  242. insecure,
  243. useCerts bool) (client *Client, err error) {
  244. fields := map[string]interface{}{
  245. "endpoint": endpoint,
  246. "insecure": insecure,
  247. "useCerts": useCerts,
  248. "version": version,
  249. }
  250. var uri *url.URL
  251. if endpoint != "" {
  252. uri, err = url.ParseRequestURI(endpoint)
  253. if err != nil {
  254. return &Client{},
  255. withFieldsE(fields, "error parsing endpoint", err)
  256. }
  257. } else {
  258. return &Client{},
  259. withFields(fields, "endpoint is required")
  260. }
  261. client = &Client{
  262. SIOEndpoint: *uri,
  263. Http: http.Client{
  264. Transport: &http.Transport{
  265. TLSHandshakeTimeout: 120 * time.Second,
  266. TLSClientConfig: &tls.Config{
  267. InsecureSkipVerify: insecure,
  268. },
  269. },
  270. },
  271. }
  272. if useCerts {
  273. pool := x509.NewCertPool()
  274. pool.AppendCertsFromPEM(pemCerts)
  275. client.Http.Transport = &http.Transport{
  276. TLSHandshakeTimeout: 120 * time.Second,
  277. TLSClientConfig: &tls.Config{
  278. RootCAs: pool,
  279. InsecureSkipVerify: insecure,
  280. },
  281. }
  282. }
  283. client.configConnect = &ConfigConnect{
  284. Version: version,
  285. }
  286. return client, nil
  287. }
  288. func GetLink(links []*types.Link, rel string) (*types.Link, error) {
  289. for _, link := range links {
  290. if link.Rel == rel {
  291. return link, nil
  292. }
  293. }
  294. return &types.Link{}, errors.New("Couldn't find link")
  295. }
  296. func withFields(fields map[string]interface{}, message string) error {
  297. return withFieldsE(fields, message, nil)
  298. }
  299. func withFieldsE(
  300. fields map[string]interface{}, message string, inner error) error {
  301. if fields == nil {
  302. fields = make(map[string]interface{})
  303. }
  304. if inner != nil {
  305. fields["inner"] = inner
  306. }
  307. x := 0
  308. l := len(fields)
  309. var b bytes.Buffer
  310. for k, v := range fields {
  311. if x < l-1 {
  312. b.WriteString(fmt.Sprintf("%s=%v,", k, v))
  313. } else {
  314. b.WriteString(fmt.Sprintf("%s=%v", k, v))
  315. }
  316. x = x + 1
  317. }
  318. return newf("%s %s", message, b.String())
  319. }
  320. func newf(format string, a ...interface{}) error {
  321. return errors.New(fmt.Sprintf(format, a))
  322. }