ecloud.go 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  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 ecloud
  15. import (
  16. "bytes"
  17. "context"
  18. "crypto/hmac"
  19. "crypto/sha1"
  20. "crypto/sha256"
  21. "encoding/hex"
  22. "fmt"
  23. "io"
  24. "net/http"
  25. "net/url"
  26. "os"
  27. "strconv"
  28. "strings"
  29. "yunion.io/x/jsonutils"
  30. "yunion.io/x/pkg/errors"
  31. "yunion.io/x/pkg/util/httputils"
  32. "yunion.io/x/pkg/util/stringutils"
  33. api "yunion.io/x/cloudmux/pkg/apis/compute"
  34. "yunion.io/x/cloudmux/pkg/cloudprovider"
  35. )
  36. const (
  37. CLOUD_PROVIDER_ECLOUD = api.CLOUD_PROVIDER_ECLOUD
  38. CLOUD_PROVIDER_ECLOUD_CN = "移动云"
  39. CLOUD_PROVIDER_ECLOUD_EN = "Ecloud"
  40. CLOUD_API_VERSION = "2016-12-05"
  41. ECLOUD_DEFAULT_REGION = "cn-beijing-1"
  42. )
  43. type SEcloudClientConfig struct {
  44. cpcfg cloudprovider.ProviderConfig
  45. AccessKey string
  46. Secret string
  47. debug bool
  48. }
  49. func NewEcloudClientConfig(accessKey, secret string) *SEcloudClientConfig {
  50. cfg := &SEcloudClientConfig{
  51. AccessKey: accessKey,
  52. Secret: secret,
  53. }
  54. return cfg
  55. }
  56. func (cfg *SEcloudClientConfig) SetCloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *SEcloudClientConfig {
  57. cfg.cpcfg = cpcfg
  58. return cfg
  59. }
  60. func (cfg *SEcloudClientConfig) SetDebug(debug bool) *SEcloudClientConfig {
  61. cfg.debug = debug
  62. return cfg
  63. }
  64. type SEcloudClient struct {
  65. *SEcloudClientConfig
  66. httpClient *http.Client
  67. }
  68. func NewEcloudClient(cfg *SEcloudClientConfig) (*SEcloudClient, error) {
  69. httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient()
  70. cli := &SEcloudClient{
  71. SEcloudClientConfig: cfg,
  72. httpClient: httpClient,
  73. }
  74. return cli, nil
  75. }
  76. func (self *SEcloudClient) GetAccessEnv() string {
  77. return api.CLOUD_ACCESS_ENV_ECLOUD_CHINA
  78. }
  79. func (ec *SEcloudClient) GetRegions() ([]SRegion, error) {
  80. ctx := context.Background()
  81. req := NewOpenApiRegionRequest(ECLOUD_DEFAULT_REGION, nil)
  82. ret := make([]SRegion, 0)
  83. if err := ec.doList(ctx, req.Base(), &ret); err != nil {
  84. return nil, err
  85. }
  86. for i := range ret {
  87. ret[i].client = ec
  88. }
  89. return ret, nil
  90. }
  91. func (ec *SEcloudClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) {
  92. regions, err := ec.GetRegions()
  93. if err != nil {
  94. return nil, err
  95. }
  96. iregions := make([]cloudprovider.ICloudRegion, len(regions))
  97. for i := range iregions {
  98. iregions[i] = &regions[i]
  99. }
  100. return iregions, nil
  101. }
  102. func (ec *SEcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
  103. iregions, err := ec.GetIRegions()
  104. if err != nil {
  105. return nil, err
  106. }
  107. for i := range iregions {
  108. if iregions[i].GetGlobalId() == id {
  109. return iregions[i], nil
  110. }
  111. }
  112. return nil, cloudprovider.ErrNotFound
  113. }
  114. func (ec *SEcloudClient) GetRegionById(id string) (*SRegion, error) {
  115. iregions, err := ec.GetIRegions()
  116. if err != nil {
  117. return nil, err
  118. }
  119. for i := range iregions {
  120. if iregions[i].GetId() == id {
  121. return iregions[i].(*SRegion), nil
  122. }
  123. }
  124. return nil, cloudprovider.ErrNotFound
  125. }
  126. func (ec *SEcloudClient) GetCapabilities() []string {
  127. caps := []string{
  128. cloudprovider.CLOUD_CAPABILITY_COMPUTE + cloudprovider.READ_ONLY_SUFFIX,
  129. cloudprovider.CLOUD_CAPABILITY_NETWORK + cloudprovider.READ_ONLY_SUFFIX,
  130. cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP + cloudprovider.READ_ONLY_SUFFIX,
  131. cloudprovider.CLOUD_CAPABILITY_EIP + cloudprovider.READ_ONLY_SUFFIX,
  132. }
  133. return caps
  134. }
  135. func (ec *SEcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
  136. subAccount := cloudprovider.SSubAccount{}
  137. subAccount.Id = ec.GetAccountId()
  138. subAccount.Name = ec.cpcfg.Name
  139. subAccount.Account = ec.AccessKey
  140. subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
  141. return []cloudprovider.SSubAccount{subAccount}, nil
  142. }
  143. func (ec *SEcloudClient) GetAccountId() string {
  144. return ec.AccessKey
  145. }
  146. // GetBalance 查询账户余额,使用 MOPC 开放接口(与 ecloudsdkmopc BalanceQueryPOST 一致)。
  147. func (ec *SEcloudClient) GetBalance() (*cloudprovider.SBalanceInfo, error) {
  148. type accountInfo struct {
  149. AccountId string `json:"accountId"`
  150. Balance string `json:"balance"`
  151. OweAmount string `json:"oweAmount"`
  152. NABalance string `json:"nABalance"`
  153. DetailName string `json:"detailName"`
  154. DetailValue string `json:"detailValue"`
  155. }
  156. type accMegRsp struct {
  157. AccountInfo []accountInfo `json:"accountInfo"`
  158. RspCode string `json:"rspCode"`
  159. RspDesc string `json:"rspDesc"`
  160. }
  161. type resultBody struct {
  162. RspCode string `json:"rspCode"`
  163. RspDesc string `json:"rspDesc"`
  164. AccMegRsp accMegRsp `json:"accMegRsp"`
  165. }
  166. type mopcResp struct {
  167. RespCode string `json:"respCode"`
  168. RespDesc string `json:"respDesc"`
  169. Result resultBody `json:"result"`
  170. }
  171. ctx := context.Background()
  172. regionId := ECLOUD_DEFAULT_REGION
  173. req := NewOpenApiMopcBalanceRequest(regionId, ec.GetAccountId())
  174. base := req.Base()
  175. base.Method = "POST"
  176. body, err := ec.doRequestRaw(ctx, base)
  177. if err != nil {
  178. return nil, err
  179. }
  180. resp := mopcResp{}
  181. if err := body.Unmarshal(&resp); err != nil {
  182. return nil, errors.Wrap(err, "unmarshal mopc balance response")
  183. }
  184. // 顶层 respCode: "0"/"00" 视为成功
  185. if resp.RespCode != "" && resp.RespCode != "0" && resp.RespCode != "00" {
  186. return nil, fmt.Errorf("balance query failed: respCode=%s respDesc=%s", resp.RespCode, resp.RespDesc)
  187. }
  188. // result.rspCode: "00"/"0000" 视为成功
  189. if resp.Result.RspCode != "" && resp.Result.RspCode != "00" && resp.Result.RspCode != "0000" {
  190. return nil, fmt.Errorf("balance result error: rspCode=%s rspDesc=%s", resp.Result.RspCode, resp.Result.RspDesc)
  191. }
  192. amount := 0.0
  193. if len(resp.Result.AccMegRsp.AccountInfo) > 0 {
  194. balanceStr := resp.Result.AccMegRsp.AccountInfo[0].Balance
  195. if balanceStr != "" {
  196. if v, err := strconv.ParseFloat(balanceStr, 64); err == nil {
  197. amount = v
  198. }
  199. }
  200. }
  201. return &cloudprovider.SBalanceInfo{
  202. Currency: "CNY",
  203. Amount: amount,
  204. Status: "",
  205. }, nil
  206. }
  207. func (ec *SEcloudClient) GetCloudRegionExternalIdPrefix() string {
  208. return CLOUD_PROVIDER_ECLOUD
  209. }
  210. // completeSingParams 填充签名相关的公共 query 参数。
  211. func (ec *SEcloudClient) completeSingParams(request *SBaseRequest) (err error) {
  212. queryParams := request.GetQueryParams()
  213. // 每次签名前先清理旧的 Signature,避免在同一个 request 上重复签名(如分页循环)时将旧签名参与新的签名计算。
  214. delete(queryParams, "Signature")
  215. queryParams["AccessKey"] = ec.AccessKey
  216. queryParams["Version"] = request.GetVersion()
  217. queryParams["Timestamp"] = request.GetTimestamp()
  218. queryParams["SignatureMethod"] = "HmacSHA1"
  219. queryParams["SignatureVersion"] = "V2.0"
  220. queryParams["SignatureNonce"] = stringutils.UUID4()
  221. return
  222. }
  223. // buildStringToSign 生成签名字符串,兼容老版移动云签名规则。
  224. func (ec *SEcloudClient) buildStringToSign(request *SBaseRequest) string {
  225. signParams := request.GetQueryParams()
  226. queryString := getUrlFormedMap(signParams)
  227. queryString = strings.Replace(queryString, "+", "%20", -1)
  228. queryString = strings.Replace(queryString, "*", "%2A", -1)
  229. queryString = strings.Replace(queryString, "%7E", "~", -1)
  230. shaString := sha256.Sum256([]byte(queryString))
  231. summaryQuery := hex.EncodeToString(shaString[:])
  232. serverPath := strings.Replace(request.GetServerPath(), "/", "%2F", -1)
  233. return fmt.Sprintf("%s\n%s\n%s", request.GetMethod(), serverPath, summaryQuery)
  234. }
  235. func signSHA1HMAC(source, secret string) string {
  236. key := []byte(secret)
  237. h := hmac.New(sha1.New, key)
  238. h.Write([]byte(source))
  239. signedBytes := h.Sum(nil)
  240. return hex.EncodeToString(signedBytes)
  241. }
  242. // parseBodyToList 统一从 API 返回的 body 中解析列表,兼容 content / regions 或直接为数组,避免各处重复处理。
  243. func parseBodyToList(body jsonutils.JSONObject) (*jsonutils.JSONArray, error) {
  244. if body == nil {
  245. return nil, fmt.Errorf("response body is nil")
  246. }
  247. if arr, ok := body.(*jsonutils.JSONArray); ok {
  248. return arr, nil
  249. }
  250. if body.Contains("content") {
  251. content, _ := body.Get("content")
  252. if arr, ok := content.(*jsonutils.JSONArray); ok {
  253. return arr, nil
  254. }
  255. // content 为 null 或 empty:true 时视为空列表
  256. if content == nil || (body.Contains("empty") && body.Contains("total")) {
  257. return jsonutils.NewArray(), nil
  258. }
  259. }
  260. if body.Contains("regions") {
  261. regions, _ := body.Get("regions")
  262. if arr, ok := regions.(*jsonutils.JSONArray); ok {
  263. return arr, nil
  264. }
  265. }
  266. if body.Contains("zones") {
  267. zones, _ := body.Get("zones")
  268. if arr, ok := zones.(*jsonutils.JSONArray); ok {
  269. return arr, nil
  270. }
  271. }
  272. return nil, fmt.Errorf("response body should be array or contain content/regions/zones array, got:\n%s", body)
  273. }
  274. func (ec *SEcloudClient) doGet(ctx context.Context, r *SBaseRequest, result interface{}) error {
  275. r.SetMethod("GET")
  276. data, err := ec.request(ctx, r)
  277. if err != nil {
  278. return err
  279. }
  280. return data.Unmarshal(result)
  281. }
  282. func (ec *SEcloudClient) doPost(ctx context.Context, r *SBaseRequest, result interface{}) error {
  283. r.SetMethod("POST")
  284. data, err := ec.request(ctx, r)
  285. if err != nil {
  286. return err
  287. }
  288. return data.Unmarshal(result)
  289. }
  290. func (ec *SEcloudClient) doList(ctx context.Context, r *SBaseRequest, result interface{}) error {
  291. r.SetMethod("GET")
  292. // doList 会自动翻页;为避免修改调用方传入的 request,这里拷贝一份 query 参数用于翻页循环。
  293. query := map[string]string{}
  294. for k, v := range r.GetQueryParams() {
  295. query[k] = v
  296. }
  297. origQuery := r.QueryParams
  298. defer func() { r.QueryParams = origQuery }()
  299. pageStr, hasPage := query["page"]
  300. pageSizeStr, hasPageSize := query["pageSize"]
  301. // 使用 page/pageSize 做简单分页聚合:
  302. // - 若调用方未显式设置 page/pageSize,则默认 page=1,pageSize=100,并自动翻页,直至返回为空或不足一页。
  303. page := 1
  304. if hasPage {
  305. if v, err := strconv.Atoi(pageStr); err == nil && v > 0 {
  306. page = v
  307. }
  308. }
  309. pageSize := 100
  310. if hasPageSize {
  311. if v, err := strconv.Atoi(pageSizeStr); err == nil && v > 0 {
  312. pageSize = v
  313. }
  314. }
  315. all := jsonutils.NewArray()
  316. for {
  317. query["page"] = strconv.Itoa(page)
  318. query["pageSize"] = strconv.Itoa(pageSize)
  319. r.QueryParams = query
  320. data, err := ec.request(ctx, r)
  321. if err != nil {
  322. return err
  323. }
  324. arr, err := parseBodyToList(data)
  325. if err != nil {
  326. return err
  327. }
  328. if arr.Length() == 0 {
  329. break
  330. }
  331. for i := 0; i < arr.Length(); i++ {
  332. item, _ := arr.GetAt(i)
  333. all.Add(item)
  334. }
  335. if arr.Length() < pageSize {
  336. break
  337. }
  338. page++
  339. }
  340. return all.Unmarshal(result)
  341. }
  342. // doPostList 与 doList 类似,但使用 POST 方法,适配新的 OpenAPI 列表接口。
  343. func (ec *SEcloudClient) doPostList(ctx context.Context, r *SBaseRequest, result interface{}) error {
  344. r.SetMethod("POST")
  345. // POST 列表接口分页参数可能在:
  346. // - 最外层:{"page":1,"pageSize":100,...}
  347. // 这里自动翻页聚合:若未显式设置,则默认 page=1,pageSize=100。
  348. origContent := r.Content
  349. defer func() { r.Content = origContent }()
  350. // doPostList 会自动翻页;为避免修改调用方传入的 request,这里基于 Content/默认值构造并循环写回 r.Content。
  351. var reqBody jsonutils.JSONObject
  352. if len(r.Content) > 0 {
  353. if jb, err := jsonutils.Parse(r.Content); err == nil {
  354. reqBody = jb
  355. }
  356. }
  357. if reqBody == nil {
  358. reqBody = jsonutils.NewDict()
  359. }
  360. // 注意:这里的 dict 是从 Content parse 出来的独立对象,不会影响调用方原始 JSON 对象。
  361. dict, ok := reqBody.(*jsonutils.JSONDict)
  362. if !ok {
  363. return errors.Errorf("doPostList request body should be JSON object, got: %s", reqBody)
  364. }
  365. page := 1
  366. pageSize := 100
  367. // 优先读取最外层 page/pageSize
  368. if v, err := dict.Int("page"); err == nil && v > 0 {
  369. page = int(v)
  370. }
  371. if v, err := dict.Int("pageSize"); err == nil && v > 0 {
  372. pageSize = int(v)
  373. }
  374. // 写回分页参数:统一更新/写入最外层 page/pageSize。
  375. setPage := func(p, ps int) {
  376. dict.Set("page", jsonutils.NewInt(int64(p)))
  377. dict.Set("pageSize", jsonutils.NewInt(int64(ps)))
  378. }
  379. // 兜底:确保 pageSize 合法
  380. if pageSize <= 0 {
  381. pageSize = 100
  382. }
  383. if page <= 0 {
  384. page = 1
  385. }
  386. all := jsonutils.NewArray()
  387. for {
  388. setPage(page, pageSize)
  389. r.Content = []byte(dict.String())
  390. data, err := ec.request(ctx, r)
  391. if err != nil {
  392. return err
  393. }
  394. arr, err := parseBodyToList(data)
  395. if err != nil {
  396. return err
  397. }
  398. if arr.Length() == 0 {
  399. break
  400. }
  401. for i := 0; i < arr.Length(); i++ {
  402. item, _ := arr.GetAt(i)
  403. all.Add(item)
  404. }
  405. if arr.Length() < pageSize {
  406. break
  407. }
  408. page++
  409. }
  410. return all.Unmarshal(result)
  411. }
  412. func (ec *SEcloudClient) request(ctx context.Context, r *SBaseRequest) (jsonutils.JSONObject, error) {
  413. jrbody, err := ec.doRequest(ctx, r)
  414. if err != nil {
  415. return nil, err
  416. }
  417. return r.ForMateResponseBody(jrbody)
  418. }
  419. // doRequestRaw 返回原始响应 body(不经过 ForMateResponseBody),用于 MOPC 等返回格式与 state/body 不同的接口。
  420. func (ec *SEcloudClient) doRequestRaw(ctx context.Context, r *SBaseRequest) (jsonutils.JSONObject, error) {
  421. return ec.doRequest(ctx, r)
  422. }
  423. func (ec *SEcloudClient) doRequest(ctx context.Context, r *SBaseRequest) (jsonutils.JSONObject, error) {
  424. // sign
  425. ec.completeSingParams(r)
  426. stringToSign := ec.buildStringToSign(r)
  427. secret := "BC_SIGNATURE&" + ec.Secret
  428. signature := signSHA1HMAC(stringToSign, secret)
  429. query := r.GetQueryParams()
  430. query["Signature"] = signature
  431. header := r.GetHeaders()
  432. header["Content-Type"] = "application/json"
  433. var urlStr string
  434. port := r.GetPort()
  435. if len(port) > 0 {
  436. urlStr = fmt.Sprintf("https://%s:%s%s", r.GetEndpoint(), port, r.GetServerPath())
  437. } else {
  438. urlStr = fmt.Sprintf("https://%s%s", r.GetEndpoint(), r.GetServerPath())
  439. }
  440. // 注意:URL query 需要与签名参数一致并进行标准转义,避免出现特殊字符解析/签名不一致问题。
  441. queryString := getUrlFormedMap(r.GetQueryParams())
  442. if len(queryString) > 0 {
  443. urlStr = urlStr + "?" + queryString
  444. }
  445. resp, err := httputils.Request(
  446. ec.httpClient,
  447. ctx,
  448. httputils.THttpMethod(r.GetMethod()),
  449. urlStr,
  450. convertHeader(header),
  451. r.GetBodyReader(),
  452. ec.debug,
  453. )
  454. defer httputils.CloseResponse(resp)
  455. if err != nil {
  456. return nil, err
  457. }
  458. rbody, err := io.ReadAll(resp.Body)
  459. if err != nil {
  460. return nil, errors.Wrap(err, "unable to read body of response")
  461. }
  462. if ec.debug {
  463. fmt.Fprintf(os.Stderr, "Response body: %s\n", string(rbody))
  464. }
  465. rbody = bytes.TrimSpace(rbody)
  466. var jrbody jsonutils.JSONObject
  467. if len(rbody) > 0 && (rbody[0] == '{' || rbody[0] == '[') {
  468. var err error
  469. jrbody, err = jsonutils.Parse(rbody)
  470. if err != nil {
  471. return nil, errors.Wrapf(err, "unable to parsing json: %s", rbody)
  472. }
  473. }
  474. return jrbody, nil
  475. }
  476. type ErrMissKey struct {
  477. Key string
  478. Jo jsonutils.JSONObject
  479. }
  480. func (mk ErrMissKey) Error() string {
  481. return fmt.Sprintf("The response body should contain the %q key, but it doesn't. It is:\n%s", mk.Key, mk.Jo)
  482. }
  483. func convertHeader(mh map[string]string) http.Header {
  484. header := http.Header{}
  485. for k, v := range mh {
  486. header.Add(k, v)
  487. }
  488. return header
  489. }
  490. func getUrlFormedMap(source map[string]string) (urlEncoded string) {
  491. urlEncoder := url.Values{}
  492. for key, value := range source {
  493. urlEncoder.Add(key, value)
  494. }
  495. urlEncoded = urlEncoder.Encode()
  496. return
  497. }