cucloud.go 10.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386
  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 cucloud
  15. import (
  16. "bytes"
  17. "context"
  18. "crypto/hmac"
  19. "crypto/sha256"
  20. "crypto/tls"
  21. "encoding/hex"
  22. "fmt"
  23. "io/ioutil"
  24. "net/http"
  25. "net/url"
  26. "sort"
  27. "strings"
  28. "sync"
  29. "time"
  30. "yunion.io/x/jsonutils"
  31. "yunion.io/x/pkg/errors"
  32. "yunion.io/x/pkg/gotypes"
  33. "yunion.io/x/pkg/util/httputils"
  34. "yunion.io/x/s3cli"
  35. api "yunion.io/x/cloudmux/pkg/apis/compute"
  36. "yunion.io/x/cloudmux/pkg/cloudprovider"
  37. )
  38. const (
  39. CLOUD_PROVIDER_CUCLOUD_CN = "联通云"
  40. CUCLOUD_DEFAULT_REGION = "cn-langfang-2"
  41. )
  42. type ChinaUnionClientConfig struct {
  43. cpcfg cloudprovider.ProviderConfig
  44. accessKeyId string
  45. accessKeySecret string
  46. debug bool
  47. }
  48. type SChinaUnionClient struct {
  49. *ChinaUnionClientConfig
  50. client *http.Client
  51. lock sync.Mutex
  52. ctx context.Context
  53. regions []SRegion
  54. ownerId string
  55. }
  56. func NewChinaUnionClientConfig(accessKeyId, accessKeySecret string) *ChinaUnionClientConfig {
  57. cfg := &ChinaUnionClientConfig{
  58. accessKeyId: accessKeyId,
  59. accessKeySecret: accessKeySecret,
  60. }
  61. return cfg
  62. }
  63. func (self *ChinaUnionClientConfig) Debug(debug bool) *ChinaUnionClientConfig {
  64. self.debug = debug
  65. return self
  66. }
  67. func (self *ChinaUnionClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *ChinaUnionClientConfig {
  68. self.cpcfg = cpcfg
  69. return self
  70. }
  71. func NewChinaUnionClient(cfg *ChinaUnionClientConfig) (*SChinaUnionClient, error) {
  72. client := &SChinaUnionClient{
  73. ChinaUnionClientConfig: cfg,
  74. ctx: context.Background(),
  75. }
  76. client.ctx = context.WithValue(client.ctx, "time", time.Now())
  77. var err error
  78. client.regions, err = client.GetRegions()
  79. if err != nil {
  80. return nil, err
  81. }
  82. return client, nil
  83. }
  84. func (self *SChinaUnionClient) GetRegions() ([]SRegion, error) {
  85. if len(self.regions) > 0 {
  86. return self.regions, nil
  87. }
  88. resp, err := self.list("/instance/v1/product/cloudregions", nil)
  89. if err != nil {
  90. return nil, err
  91. }
  92. ret := []SRegion{}
  93. err = resp.Unmarshal(&ret)
  94. if err != nil {
  95. return nil, err
  96. }
  97. self.regions = []SRegion{}
  98. for i := range ret {
  99. ret[i].client = self
  100. self.regions = append(self.regions, ret[i])
  101. }
  102. return self.regions, nil
  103. }
  104. func (self *SChinaUnionClient) GetRegion(id string) (*SRegion, error) {
  105. regions, err := self.GetRegions()
  106. if err != nil {
  107. return nil, err
  108. }
  109. for i := range regions {
  110. if regions[i].GetId() == id || regions[i].GetGlobalId() == id {
  111. regions[i].client = self
  112. return &regions[i], nil
  113. }
  114. }
  115. return nil, cloudprovider.ErrNotFound
  116. }
  117. func (self *SChinaUnionClient) getUrl(resource string) string {
  118. return fmt.Sprintf("https://gateway.cucloud.cn/%s", strings.TrimPrefix(resource, "/"))
  119. }
  120. func (cli *SChinaUnionClient) getDefaultClient() *http.Client {
  121. cli.lock.Lock()
  122. defer cli.lock.Unlock()
  123. if !gotypes.IsNil(cli.client) {
  124. return cli.client
  125. }
  126. cli.client = httputils.GetAdaptiveTimeoutClient()
  127. httputils.SetClientProxyFunc(cli.client, cli.cpcfg.ProxyFunc)
  128. ts, _ := cli.client.Transport.(*http.Transport)
  129. ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
  130. cli.client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) {
  131. if cli.cpcfg.ReadOnly {
  132. if req.Method == "GET" {
  133. return nil, nil
  134. }
  135. return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
  136. }
  137. return nil, nil
  138. })
  139. return cli.client
  140. }
  141. type sChinaUnionError struct {
  142. StatusCode int `json:"statusCode"`
  143. Status string
  144. Code string
  145. Message string
  146. }
  147. func (self *sChinaUnionError) Error() string {
  148. return jsonutils.Marshal(self).String()
  149. }
  150. func (self *sChinaUnionError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
  151. if body != nil {
  152. body.Unmarshal(self)
  153. }
  154. self.StatusCode = statusCode
  155. return self
  156. }
  157. func (self *SChinaUnionClient) sign(req *http.Request) (string, error) {
  158. keys := []string{}
  159. keyMap := map[string]string{}
  160. for k := range req.Header {
  161. key, ok := map[string]string{
  162. "Accesskey": "accessKey",
  163. "Algorithm": "algorithm",
  164. "Requesttime": "requestTime",
  165. }[k]
  166. if ok {
  167. keys = append(keys, key)
  168. keyMap[key] = req.Header.Get(k)
  169. }
  170. }
  171. params, err := url.ParseQuery(req.URL.RawQuery)
  172. if err != nil {
  173. return "", errors.Wrapf(err, "ParseQuery")
  174. }
  175. for k := range params {
  176. keys = append(keys, k)
  177. keyMap[k] = params.Get(k)
  178. }
  179. if req.Method == "POST" {
  180. body, err := ioutil.ReadAll(req.Body)
  181. if err != nil {
  182. return "", errors.Wrapf(err, "read body")
  183. }
  184. req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
  185. obj, err := jsonutils.Parse(body)
  186. if err != nil {
  187. return "", errors.Wrapf(err, "params req body")
  188. }
  189. objMap, err := obj.GetMap()
  190. if err != nil {
  191. return "", errors.Wrapf(err, "req body map")
  192. }
  193. for k := range objMap {
  194. keys = append(keys, k)
  195. keyMap[k], _ = objMap[k].GetString()
  196. }
  197. }
  198. sort.Strings(keys)
  199. signStrs := []string{}
  200. for _, k := range keys {
  201. signStrs = append(signStrs, fmt.Sprintf(`%s="%s"`, k, keyMap[k]))
  202. }
  203. hasher := hmac.New(sha256.New, []byte(self.accessKeySecret))
  204. hasher.Write([]byte(strings.Join(signStrs, "&")))
  205. return hex.EncodeToString(hasher.Sum(nil)), nil
  206. }
  207. func (self *SChinaUnionClient) Do(req *http.Request) (*http.Response, error) {
  208. client := self.getDefaultClient()
  209. req.Header.Set("Content-Type", "application/json")
  210. req.Header.Set("Accept", "application/json")
  211. req.Header.Set("algorithm", "HmacSHA256")
  212. req.Header.Set("requestTime", fmt.Sprintf("%d", time.Now().UTC().UnixMilli()))
  213. req.Header.Set("accessKey", self.accessKeyId)
  214. signature, err := self.sign(req)
  215. if err != nil {
  216. return nil, errors.Wrapf(err, "sign")
  217. }
  218. req.Header.Set("sign", signature)
  219. return client.Do(req)
  220. }
  221. func (self *SChinaUnionClient) list(resource string, params url.Values) (jsonutils.JSONObject, error) {
  222. return self._list(resource, params)
  223. }
  224. func (self *SChinaUnionClient) _list(resource string, params url.Values) (jsonutils.JSONObject, error) {
  225. ret := jsonutils.NewArray()
  226. if gotypes.IsNil(params) {
  227. params = url.Values{}
  228. }
  229. pageNum := 1
  230. pageSize := 100
  231. for {
  232. params.Set("pageNum", fmt.Sprintf("%d", pageNum))
  233. params.Set("pageSize", fmt.Sprintf("%d", pageSize))
  234. resp, err := self.request(httputils.GET, resource, params, nil)
  235. if err != nil {
  236. return nil, err
  237. }
  238. part := struct {
  239. Result struct {
  240. Total int
  241. List []jsonutils.JSONObject
  242. }
  243. }{}
  244. err = resp.Unmarshal(&part)
  245. if err != nil {
  246. return nil, err
  247. }
  248. ret.Add(part.Result.List...)
  249. if len(part.Result.List) == 0 || ret.Length() >= part.Result.Total {
  250. break
  251. }
  252. pageNum++
  253. }
  254. return ret, nil
  255. }
  256. func (self *SChinaUnionClient) get(resource string) (jsonutils.JSONObject, error) {
  257. return self.request(httputils.GET, resource, nil, nil)
  258. }
  259. func (self *SChinaUnionClient) post(resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  260. return self.request(httputils.POST, resource, nil, params)
  261. }
  262. func (self *SChinaUnionClient) request(method httputils.THttpMethod, resource string, query url.Values, params map[string]interface{}) (jsonutils.JSONObject, error) {
  263. uri := self.getUrl(resource)
  264. if params == nil {
  265. params = map[string]interface{}{}
  266. }
  267. if len(query) > 0 {
  268. uri = fmt.Sprintf("%s?%s", uri, query.Encode())
  269. }
  270. var body jsonutils.JSONObject = jsonutils.NewDict()
  271. if len(params) > 0 {
  272. body = jsonutils.Marshal(params)
  273. }
  274. req := httputils.NewJsonRequest(method, uri, body)
  275. bErr := &sChinaUnionError{}
  276. client := httputils.NewJsonClient(self)
  277. _, resp, err := client.Send(self.ctx, req, bErr, self.debug)
  278. if err != nil {
  279. return nil, err
  280. }
  281. if gotypes.IsNil(resp) {
  282. return nil, fmt.Errorf("empty response")
  283. }
  284. code, _ := resp.GetString("code")
  285. if code != "200" {
  286. return nil, errors.Errorf("%s", resp.String())
  287. }
  288. return resp, nil
  289. }
  290. func (self *SChinaUnionClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
  291. subAccount := cloudprovider.SSubAccount{}
  292. subAccount.Id = self.GetAccountId()
  293. subAccount.Name = self.cpcfg.Name
  294. subAccount.Account = self.accessKeyId
  295. subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
  296. return []cloudprovider.SSubAccount{subAccount}, nil
  297. }
  298. func (self *SChinaUnionClient) getOwnerId() (string, error) {
  299. if len(self.ownerId) > 0 {
  300. return self.ownerId, nil
  301. }
  302. client, err := self.getS3Client()
  303. if err != nil {
  304. return "", err
  305. }
  306. buckets, err := client.ListBuckets()
  307. if err != nil {
  308. return "", err
  309. }
  310. self.ownerId = buckets.Owner.ID
  311. return self.ownerId, nil
  312. }
  313. func (self *SChinaUnionClient) getS3Client() (*s3cli.Client, error) {
  314. client, err := s3cli.New("obs-helf.cucloud.cn", self.accessKeyId, self.accessKeySecret, true, self.debug)
  315. tr := httputils.GetTransport(true)
  316. tr.Proxy = self.cpcfg.ProxyFunc
  317. return client, err
  318. }
  319. func (self *SChinaUnionClient) GetAccountId() string {
  320. ownerId, _ := self.getOwnerId()
  321. return ownerId
  322. }
  323. type CashBalance struct {
  324. CashBalance float64
  325. }
  326. // 接口不可用
  327. func (self *SChinaUnionClient) QueryBalance() (*CashBalance, error) {
  328. ret := &CashBalance{}
  329. resp, err := self.post("bill-manage-console/bill/manage/balance/queryAvailableBalanceDetail", nil)
  330. if err != nil {
  331. return nil, err
  332. }
  333. err = resp.Unmarshal(ret)
  334. if err != nil {
  335. return nil, errors.Wrapf(err, "resp.Unmarshal")
  336. }
  337. return ret, nil
  338. }
  339. func (self *SChinaUnionClient) GetCapabilities() []string {
  340. caps := []string{
  341. cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX,
  342. cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX,
  343. cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP + cloudprovider.READ_ONLY_SUFFIX,
  344. }
  345. return caps
  346. }