qingcloud.go 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289
  1. // Copyright 2019 Yunion
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. package qingcloud
  15. import (
  16. "context"
  17. "crypto/hmac"
  18. "crypto/sha256"
  19. "crypto/tls"
  20. "encoding/base64"
  21. "fmt"
  22. "net/http"
  23. "net/url"
  24. "sort"
  25. "strings"
  26. "sync"
  27. "time"
  28. "yunion.io/x/jsonutils"
  29. "yunion.io/x/pkg/errors"
  30. "yunion.io/x/pkg/gotypes"
  31. "yunion.io/x/pkg/util/httputils"
  32. api "yunion.io/x/cloudmux/pkg/apis/compute"
  33. "yunion.io/x/cloudmux/pkg/cloudprovider"
  34. )
  35. const (
  36. CLOUD_PROVIDER_QINGCLOUD_CN = "青云"
  37. QINGCLOUD_DEFAULT_REGION = "pek3"
  38. ISO8601 = "2006-01-02T15:04:05Z"
  39. )
  40. type QingCloudClientConfig struct {
  41. cpcfg cloudprovider.ProviderConfig
  42. accessKeyId string
  43. accessKeySecret string
  44. debug bool
  45. }
  46. type SQingCloudClient struct {
  47. *QingCloudClientConfig
  48. client *http.Client
  49. lock sync.Mutex
  50. ctx context.Context
  51. ownerId string
  52. }
  53. func NewQingCloudClientConfig(accessKeyId, accessKeySecret string) *QingCloudClientConfig {
  54. cfg := &QingCloudClientConfig{
  55. accessKeyId: accessKeyId,
  56. accessKeySecret: accessKeySecret,
  57. }
  58. return cfg
  59. }
  60. func (self *QingCloudClientConfig) Debug(debug bool) *QingCloudClientConfig {
  61. self.debug = debug
  62. return self
  63. }
  64. func (self *QingCloudClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *QingCloudClientConfig {
  65. self.cpcfg = cpcfg
  66. return self
  67. }
  68. func NewQingCloudClient(cfg *QingCloudClientConfig) (*SQingCloudClient, error) {
  69. client := &SQingCloudClient{
  70. QingCloudClientConfig: cfg,
  71. ctx: context.Background(),
  72. }
  73. client.ctx = context.WithValue(client.ctx, "time", time.Now())
  74. _, err := client.getOwnerId()
  75. return client, err
  76. }
  77. func (self *SQingCloudClient) GetRegions() []SRegion {
  78. ret := []SRegion{}
  79. for k, v := range regions {
  80. ret = append(ret, SRegion{
  81. client: self,
  82. Region: k,
  83. RegionName: v,
  84. })
  85. }
  86. return ret
  87. }
  88. func (self *SQingCloudClient) GetRegion(id string) (*SRegion, error) {
  89. regions := self.GetRegions()
  90. for i := range regions {
  91. if regions[i].Region == id {
  92. regions[i].client = self
  93. return &regions[i], nil
  94. }
  95. }
  96. return nil, cloudprovider.ErrNotFound
  97. }
  98. func (self *SQingCloudClient) getUrl(service string) (string, error) {
  99. switch service {
  100. case "ec2":
  101. return fmt.Sprintf("https://api.qingcloud.com/iaas/"), nil
  102. default:
  103. return "", errors.Wrapf(cloudprovider.ErrNotSupported, "%s", service)
  104. }
  105. }
  106. func (cli *SQingCloudClient) getDefaultClient() *http.Client {
  107. cli.lock.Lock()
  108. defer cli.lock.Unlock()
  109. if !gotypes.IsNil(cli.client) {
  110. return cli.client
  111. }
  112. cli.client = httputils.GetAdaptiveTimeoutClient()
  113. httputils.SetClientProxyFunc(cli.client, cli.cpcfg.ProxyFunc)
  114. ts, _ := cli.client.Transport.(*http.Transport)
  115. ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
  116. cli.client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) {
  117. if cli.cpcfg.ReadOnly {
  118. if req.Method == "GET" {
  119. return nil, nil
  120. }
  121. return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
  122. }
  123. return nil, nil
  124. })
  125. return cli.client
  126. }
  127. type sQingCloudError struct {
  128. StatusCode int `json:"statusCode"`
  129. RequestId string `json:"requestId"`
  130. Code string
  131. Message string
  132. }
  133. func (self *sQingCloudError) Error() string {
  134. return jsonutils.Marshal(self).String()
  135. }
  136. func (self *sQingCloudError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
  137. if body != nil {
  138. body.Unmarshal(self)
  139. }
  140. self.StatusCode = statusCode
  141. return self
  142. }
  143. func (self *SQingCloudClient) sign(req *http.Request) (string, error) {
  144. keys := []string{}
  145. for k := range req.URL.Query() {
  146. keys = append(keys, k)
  147. }
  148. sort.Strings(keys)
  149. parts := []string{}
  150. for _, key := range keys {
  151. parts = append(parts, fmt.Sprintf(`%s=%s`, url.QueryEscape(key), url.QueryEscape(req.URL.Query().Get(key))))
  152. }
  153. signStr := fmt.Sprintf("%s\n%s\n%s", req.Method, req.URL.Path, strings.Join(parts, "&"))
  154. hashed := hmac.New(sha256.New, []byte(self.accessKeySecret))
  155. hashed.Write([]byte(signStr))
  156. return base64.StdEncoding.EncodeToString(hashed.Sum(nil)), nil
  157. }
  158. func (self *SQingCloudClient) Do(req *http.Request) (*http.Response, error) {
  159. client := self.getDefaultClient()
  160. signature, err := self.sign(req)
  161. if err != nil {
  162. return nil, errors.Wrapf(err, "sign")
  163. }
  164. req.URL.RawQuery += fmt.Sprintf("&signature=%s", url.QueryEscape(signature))
  165. return client.Do(req)
  166. }
  167. func (self *SQingCloudClient) ec2Request(action, regionId string, params map[string]string) (jsonutils.JSONObject, error) {
  168. return self.request("ec2", action, regionId, params)
  169. }
  170. func (self *SQingCloudClient) request(service, action, regionId string, params map[string]string) (jsonutils.JSONObject, error) {
  171. uri, err := self.getUrl(service)
  172. if err != nil {
  173. return nil, err
  174. }
  175. if len(regionId) == 0 {
  176. regionId = QINGCLOUD_DEFAULT_REGION
  177. }
  178. if params == nil {
  179. params = map[string]string{}
  180. }
  181. params["action"] = action
  182. if regionId == "ap2" {
  183. regionId = "ap2a"
  184. }
  185. params["zone"] = regionId
  186. params["time_stamp"] = time.Now().Format(ISO8601)
  187. params["access_key_id"] = self.accessKeyId
  188. params["version"] = "1"
  189. params["signature_method"] = "HmacSHA256"
  190. params["signature_version"] = "1"
  191. values := url.Values{}
  192. for k, v := range params {
  193. values.Set(k, v)
  194. }
  195. uri = fmt.Sprintf("%s?%s", uri, values.Encode())
  196. req := httputils.NewJsonRequest(httputils.GET, uri, nil)
  197. bErr := &sQingCloudError{}
  198. client := httputils.NewJsonClient(self)
  199. _, resp, err := client.Send(self.ctx, req, bErr, self.debug)
  200. if err != nil {
  201. return nil, err
  202. }
  203. retCode, _ := resp.Int("ret_code")
  204. if retCode > 0 {
  205. // https://docs.qingcloud.com/product/api/common/error_code.html
  206. if retCode == 1200 {
  207. return nil, errors.Wrapf(cloudprovider.ErrInvalidAccessKey, "%s", resp.String())
  208. }
  209. return nil, errors.Errorf("%s", resp.String())
  210. }
  211. return resp, nil
  212. }
  213. func (self *SQingCloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
  214. subAccount := cloudprovider.SSubAccount{}
  215. subAccount.Id = self.GetAccountId()
  216. subAccount.Name = self.cpcfg.Name
  217. subAccount.Account = self.accessKeyId
  218. subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
  219. return []cloudprovider.SSubAccount{subAccount}, nil
  220. }
  221. func (self *SQingCloudClient) getOwnerId() (string, error) {
  222. if len(self.ownerId) > 0 {
  223. return self.ownerId, nil
  224. }
  225. _, err := self.QueryBalance()
  226. return self.ownerId, err
  227. }
  228. func (self *SQingCloudClient) GetAccountId() string {
  229. ownerId, _ := self.getOwnerId()
  230. return ownerId
  231. }
  232. type Balance struct {
  233. Balance float64
  234. RootUserId string
  235. }
  236. func (self *SQingCloudClient) QueryBalance() (*Balance, error) {
  237. resp, err := self.ec2Request("GetBalance", "", nil)
  238. if err != nil {
  239. return nil, err
  240. }
  241. ret := &Balance{}
  242. err = resp.Unmarshal(ret)
  243. if err != nil {
  244. return nil, errors.Wrapf(err, "resp.Unmarshal")
  245. }
  246. self.ownerId = ret.RootUserId
  247. return ret, nil
  248. }
  249. func (self *SQingCloudClient) GetCapabilities() []string {
  250. caps := []string{
  251. cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX,
  252. }
  253. return caps
  254. }