ksyun.go 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  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 ksyun
  15. import (
  16. "bytes"
  17. "context"
  18. "crypto/hmac"
  19. "crypto/sha256"
  20. "crypto/tls"
  21. "encoding/hex"
  22. "fmt"
  23. "io"
  24. "net/http"
  25. "net/url"
  26. "sort"
  27. "strings"
  28. "sync"
  29. "time"
  30. "github.com/aws/aws-sdk-go/aws/credentials"
  31. v4 "github.com/aws/aws-sdk-go/aws/signer/v4"
  32. "yunion.io/x/jsonutils"
  33. "yunion.io/x/pkg/errors"
  34. "yunion.io/x/pkg/gotypes"
  35. "yunion.io/x/pkg/util/httputils"
  36. api "yunion.io/x/cloudmux/pkg/apis/compute"
  37. "yunion.io/x/cloudmux/pkg/cloudprovider"
  38. )
  39. const (
  40. CLOUD_PROVIDER_KSYUN_CN = "金山云"
  41. KSYUN_DEFAULT_REGION = "cn-beijing-6"
  42. KSYUN_DEFAULT_API_VERSION = "2016-03-04"
  43. KSYUN_RDS_API_VERSION = "2016-07-01"
  44. KSYUN_SKS_API_VERSION = "2015-11-01"
  45. KSYUN_MONITOR_API_VERSION = "2018-11-14"
  46. )
  47. type KsyunClientConfig struct {
  48. cpcfg cloudprovider.ProviderConfig
  49. accessKeyId string
  50. accessKeySecret string
  51. debug bool
  52. }
  53. type SKsyunClient struct {
  54. *KsyunClientConfig
  55. client *http.Client
  56. lock sync.Mutex
  57. ctx context.Context
  58. customerId string
  59. regions []SRegion
  60. }
  61. func NewKsyunClientConfig(accessKeyId, accessKeySecret string) *KsyunClientConfig {
  62. cfg := &KsyunClientConfig{
  63. accessKeyId: accessKeyId,
  64. accessKeySecret: accessKeySecret,
  65. }
  66. return cfg
  67. }
  68. func (cli *KsyunClientConfig) Debug(debug bool) *KsyunClientConfig {
  69. cli.debug = debug
  70. return cli
  71. }
  72. func (cli *KsyunClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *KsyunClientConfig {
  73. cli.cpcfg = cpcfg
  74. return cli
  75. }
  76. func NewKsyunClient(cfg *KsyunClientConfig) (*SKsyunClient, error) {
  77. client := &SKsyunClient{
  78. KsyunClientConfig: cfg,
  79. ctx: context.Background(),
  80. }
  81. client.ctx = context.WithValue(client.ctx, "time", time.Now())
  82. var err error
  83. client.regions, err = client.GetRegions()
  84. return client, err
  85. }
  86. func (cli *SKsyunClient) GetRegions() ([]SRegion, error) {
  87. resp, err := cli.ec2Request("", "DescribeRegions", nil)
  88. if err != nil {
  89. return nil, err
  90. }
  91. ret := struct {
  92. RegionSet []SRegion
  93. }{}
  94. err = resp.Unmarshal(&ret)
  95. if err != nil {
  96. return nil, err
  97. }
  98. for i := range ret.RegionSet {
  99. ret.RegionSet[i].client = cli
  100. }
  101. return ret.RegionSet, nil
  102. }
  103. func (cli *SKsyunClient) GetRegion(id string) (*SRegion, error) {
  104. for i := range cli.regions {
  105. if cli.regions[i].GetGlobalId() == id || cli.regions[i].GetId() == id {
  106. cli.regions[i].client = cli
  107. return &cli.regions[i], nil
  108. }
  109. }
  110. return nil, cloudprovider.ErrNotFound
  111. }
  112. func (cli *SKsyunClient) getUrl(service, regionId string) (string, error) {
  113. if len(regionId) == 0 {
  114. regionId = KSYUN_DEFAULT_REGION
  115. }
  116. switch service {
  117. case "kingpay", "iam", "vpc", "ebs", "eip", "sks":
  118. return fmt.Sprintf("http://%s.api.ksyun.com", service), nil
  119. case "kec", "tag", "krds":
  120. return fmt.Sprintf("https://%s.%s.api.ksyun.com", service, regionId), nil
  121. case "monitor":
  122. return fmt.Sprintf("https://%s.api.ksyun.com", service), nil
  123. }
  124. return "", errors.Wrapf(cloudprovider.ErrNotSupported, "service %s", service)
  125. }
  126. func (cli *SKsyunClient) getDefaultClient() *http.Client {
  127. cli.lock.Lock()
  128. defer cli.lock.Unlock()
  129. if !gotypes.IsNil(cli.client) {
  130. return cli.client
  131. }
  132. cli.client = httputils.GetAdaptiveTimeoutClient()
  133. httputils.SetClientProxyFunc(cli.client, cli.cpcfg.ProxyFunc)
  134. ts, _ := cli.client.Transport.(*http.Transport)
  135. ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
  136. cli.client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) {
  137. params, err := url.ParseQuery(req.URL.RawQuery)
  138. if err != nil {
  139. return nil, errors.Wrapf(err, "ParseQuery(%s)", req.URL.RawQuery)
  140. }
  141. action := params.Get("Action")
  142. for _, prefix := range []string{"Get", "List", "Describe", "Query"} {
  143. if strings.HasPrefix(action, prefix) {
  144. return nil, nil
  145. }
  146. }
  147. // ks3
  148. if len(action) == 0 && strings.Contains(req.URL.String(), "ks3-") {
  149. return nil, nil
  150. }
  151. if cli.cpcfg.ReadOnly {
  152. return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
  153. }
  154. return nil, nil
  155. })
  156. return cli.client
  157. }
  158. // {"RequestId":"51aee78d-8c35-4778-92fb-a622c40fa5ae","Error":{"Code":"INVALID_ACTION","Message":"Not Found"}}
  159. type sKsyunError struct {
  160. Params map[string]interface{} `json:"Params"`
  161. StatusCode int `json:"StatusCode"`
  162. RequestId string `json:"RequestId"`
  163. ErrorMsg struct {
  164. Code string `json:"Code"`
  165. Message string `json:"Message"`
  166. Type string `json:"Type"`
  167. } `json:"Error"`
  168. }
  169. func (cli *sKsyunError) Error() string {
  170. return jsonutils.Marshal(cli).String()
  171. }
  172. func (cli *sKsyunError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
  173. if body != nil {
  174. body.Unmarshal(cli)
  175. }
  176. if cli.ErrorMsg.Message == "Not Found" {
  177. return errors.Wrapf(cloudprovider.ErrNotFound, "%s", jsonutils.Marshal(cli.ErrorMsg).String())
  178. }
  179. cli.StatusCode = statusCode
  180. return cli
  181. }
  182. func (cli *SKsyunClient) sign(req *http.Request) (string, error) {
  183. query, err := url.ParseQuery(req.URL.RawQuery)
  184. if err != nil {
  185. return "", err
  186. }
  187. keys := []string{}
  188. for k := range query {
  189. keys = append(keys, k)
  190. }
  191. sort.Strings(keys)
  192. var buf bytes.Buffer
  193. for i := range keys {
  194. k := keys[i]
  195. buf.WriteString(strings.Replace(url.QueryEscape(k), "+", "%20", -1))
  196. buf.WriteString("=")
  197. buf.WriteString(strings.Replace(url.QueryEscape(query.Get(k)), "+", "%20", -1))
  198. buf.WriteString("&")
  199. }
  200. buf.Truncate(buf.Len() - 1)
  201. hashed := hmac.New(sha256.New, []byte(cli.accessKeySecret))
  202. hashed.Write(buf.Bytes())
  203. return hex.EncodeToString(hashed.Sum(nil)), nil
  204. }
  205. func (cli *SKsyunClient) Do(req *http.Request) (*http.Response, error) {
  206. client := cli.getDefaultClient()
  207. req.Header.Set("Accept", "application/json")
  208. if req.Method == "POST" || req.Method == "PUT" || req.Method == "DELETE" && req.Body != nil {
  209. cred := credentials.NewStaticCredentials(cli.accessKeyId, cli.accessKeySecret, "")
  210. sig := v4.NewSigner(cred)
  211. var body io.ReadSeeker = nil
  212. bodyBytes, err := io.ReadAll(req.Body)
  213. if err != nil {
  214. return nil, errors.Wrapf(err, "ReadAll")
  215. }
  216. body = bytes.NewReader(bodyBytes)
  217. v4Req, err := http.NewRequestWithContext(cli.ctx, req.Method, req.URL.String(), body)
  218. if err != nil {
  219. return nil, errors.Wrapf(err, "NewRequestWithContext")
  220. }
  221. v4Req.Header.Set("Accept", "application/json")
  222. v4Req.Header.Set("X-Amz-Date", time.Now().UTC().Format("20060102T150405Z"))
  223. v4Req.Header.Set("Content-Type", req.Header.Get("Content-Type"))
  224. v4Req.Header.Set("Host", req.URL.Host)
  225. v4Req.Header.Set("User-Agent", req.Header.Get("User-Agent"))
  226. v4Req.ContentLength = int64(len(bodyBytes))
  227. service, regionId := "", KSYUN_DEFAULT_REGION
  228. urlInfo := strings.Split(req.URL.Host, ".")
  229. if len(urlInfo) < 2 {
  230. return nil, errors.Wrapf(errors.ErrInvalidStatus, "urlInfo")
  231. }
  232. service = urlInfo[0]
  233. if urlInfo[1] != "api" {
  234. regionId = urlInfo[1]
  235. }
  236. _, err = sig.Sign(v4Req, body, service, regionId, time.Now())
  237. if err != nil {
  238. return nil, errors.Wrapf(err, "sign")
  239. }
  240. return client.Do(v4Req)
  241. }
  242. signature, err := cli.sign(req)
  243. if err != nil {
  244. return nil, errors.Wrapf(err, "sign")
  245. }
  246. query, err := url.ParseQuery(req.URL.RawQuery)
  247. if err != nil {
  248. return nil, err
  249. }
  250. query.Set("Signature", signature)
  251. req.URL.RawQuery = query.Encode()
  252. return client.Do(req)
  253. }
  254. func (cli *SKsyunClient) ec2Request(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  255. return cli.request("kec", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params)
  256. }
  257. func (cli *SKsyunClient) iamRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  258. return cli.request("iam", regionId, apiName, "2015-11-01", params)
  259. }
  260. func (cli *SKsyunClient) tagRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  261. return cli.request("tag", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params)
  262. }
  263. func (cli *SKsyunClient) eipRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  264. return cli.request("eip", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params)
  265. }
  266. func (cli *SKsyunClient) ebsRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  267. return cli.request("ebs", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params)
  268. }
  269. func (cli *SKsyunClient) sksRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  270. return cli.request("sks", regionId, apiName, KSYUN_SKS_API_VERSION, params)
  271. }
  272. func (cli *SKsyunClient) rdsRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  273. return cli.request("krds", regionId, apiName, KSYUN_RDS_API_VERSION, params)
  274. }
  275. func (cli *SKsyunClient) vpcRequest(regionId, apiName string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  276. return cli.request("vpc", regionId, apiName, KSYUN_DEFAULT_API_VERSION, params)
  277. }
  278. func (cli *SKsyunClient) request(service, regionId, apiName, apiVersion string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  279. isQueryApi := strings.HasPrefix(apiName, "Get") || strings.HasPrefix(apiName, "Describe") || strings.HasPrefix(apiName, "List")
  280. if !isQueryApi {
  281. return cli._request(service, regionId, apiName, apiVersion, params)
  282. }
  283. for i := 0; i < 2; i++ {
  284. resp, err := cli._request(service, regionId, apiName, apiVersion, params)
  285. if err != nil {
  286. retry := false
  287. for _, key := range []string{
  288. "EOF",
  289. "i/o timeout",
  290. "TLS handshake timeout",
  291. "connection reset by peer",
  292. } {
  293. if strings.Contains(err.Error(), key) {
  294. retry = true
  295. break
  296. }
  297. }
  298. if !retry {
  299. return nil, errors.Wrapf(err, "request")
  300. }
  301. time.Sleep(time.Second * 10)
  302. continue
  303. }
  304. return resp, nil
  305. }
  306. return cli._request(service, regionId, apiName, apiVersion, params)
  307. }
  308. func (cli *SKsyunClient) _request(service, regionId, apiName, apiVersion string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  309. uri, err := cli.getUrl(service, regionId)
  310. if err != nil {
  311. return nil, errors.Wrapf(err, "getUrl")
  312. }
  313. if params == nil {
  314. params = map[string]interface{}{}
  315. }
  316. values := url.Values{}
  317. values.Set("Action", apiName)
  318. values.Set("Version", apiVersion)
  319. values.Set("Service", service)
  320. method := httputils.GET
  321. if apiName == "GetMetricStatisticsBatch" {
  322. method = httputils.POST
  323. }
  324. if method == httputils.GET {
  325. values.Set("Accesskey", cli.accessKeyId)
  326. values.Set("SignatureMethod", "HMAC-SHA256")
  327. values.Set("Format", "json")
  328. values.Set("SignatureVersion", "1.0")
  329. values.Set("Timestamp", time.Now().UTC().Format("2006-01-02T15:04:05Z"))
  330. if len(regionId) > 0 {
  331. values.Set("Region", regionId)
  332. }
  333. }
  334. ksErr := &sKsyunError{Params: params}
  335. if method == httputils.GET {
  336. for k, v := range params {
  337. values.Set(k, fmt.Sprintf("%v", v))
  338. }
  339. params = nil
  340. }
  341. uri = fmt.Sprintf("%s?%s", uri, values.Encode())
  342. req := httputils.NewJsonRequest(method, uri, params)
  343. client := httputils.NewJsonClient(cli)
  344. _, resp, err := client.Send(cli.ctx, req, ksErr, cli.debug)
  345. if err != nil {
  346. return nil, err
  347. }
  348. if info, err := resp.GetMap(); err == nil {
  349. for k, v := range info {
  350. if strings.HasSuffix(k, "Result") {
  351. return v, nil
  352. }
  353. }
  354. }
  355. return resp, nil
  356. }
  357. func (cli *SKsyunClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
  358. subAccount := cloudprovider.SSubAccount{}
  359. subAccount.Id = cli.GetAccountId()
  360. subAccount.Name = cli.cpcfg.Name
  361. subAccount.Account = cli.accessKeyId
  362. subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
  363. return []cloudprovider.SSubAccount{subAccount}, nil
  364. }
  365. func (cli *SKsyunClient) GetAccountId() string {
  366. if len(cli.customerId) > 0 {
  367. return cli.customerId
  368. }
  369. cli.QueryCashWalletAction()
  370. return cli.customerId
  371. }
  372. type CashWalletDetail struct {
  373. CustomerId string
  374. AvailableAmount float64
  375. RewardAmount string
  376. FrozenAmount string
  377. Currency string
  378. }
  379. func (cli *SKsyunClient) QueryCashWalletAction() (*CashWalletDetail, error) {
  380. resp, err := cli.request("kingpay", "", "QueryCashWalletAction", "V1", nil)
  381. if err != nil {
  382. return nil, err
  383. }
  384. ret := &CashWalletDetail{}
  385. err = resp.Unmarshal(ret, "data")
  386. if err != nil {
  387. return nil, errors.Wrapf(err, "resp.Unmarshal")
  388. }
  389. cli.customerId = ret.CustomerId
  390. return ret, nil
  391. }
  392. func (cli *SKsyunClient) GetCapabilities() []string {
  393. caps := []string{
  394. cloudprovider.CLOUD_CAPABILITY_COMPUTE,
  395. cloudprovider.CLOUD_CAPABILITY_PROJECT,
  396. cloudprovider.CLOUD_CAPABILITY_CLOUDID,
  397. cloudprovider.CLOUD_CAPABILITY_NETWORK,
  398. cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP,
  399. cloudprovider.CLOUD_CAPABILITY_EIP,
  400. cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE,
  401. cloudprovider.CLOUD_CAPABILITY_RDS + cloudprovider.READ_ONLY_SUFFIX,
  402. }
  403. return caps
  404. }