ctyun.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  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 ctyun
  15. import (
  16. "bytes"
  17. "context"
  18. "crypto/hmac"
  19. "crypto/sha256"
  20. "crypto/tls"
  21. "encoding/base64"
  22. "encoding/hex"
  23. "fmt"
  24. "io/ioutil"
  25. "net/http"
  26. "net/url"
  27. "sort"
  28. "strings"
  29. "sync"
  30. "time"
  31. "yunion.io/x/jsonutils"
  32. "yunion.io/x/log"
  33. "yunion.io/x/pkg/errors"
  34. "yunion.io/x/pkg/gotypes"
  35. "yunion.io/x/pkg/util/httputils"
  36. "yunion.io/x/pkg/utils"
  37. api "yunion.io/x/cloudmux/pkg/apis/compute"
  38. "yunion.io/x/cloudmux/pkg/cloudprovider"
  39. )
  40. const (
  41. CLOUD_PROVIDER_CTYUN = api.CLOUD_PROVIDER_CTYUN
  42. CLOUD_PROVIDER_CTYUN_CN = "天翼云"
  43. CLOUD_PROVIDER_CTYUN_EN = CLOUD_PROVIDER_CTYUN
  44. SERVICE_ECS = "ecs"
  45. SERVICE_VPC = "vpc"
  46. SERVICE_IMAGE = "image"
  47. SERVICE_ACCT = "acct"
  48. SERVICE_EBS = "ebs"
  49. )
  50. type CtyunClientConfig struct {
  51. cpcfg cloudprovider.ProviderConfig
  52. projectId string
  53. accessKey string
  54. accessSecret string
  55. debug bool
  56. }
  57. func NewSCtyunClientConfig(accessKey, accessSecret string) *CtyunClientConfig {
  58. cfg := &CtyunClientConfig{
  59. accessKey: accessKey,
  60. accessSecret: accessSecret,
  61. }
  62. return cfg
  63. }
  64. func (cfg *CtyunClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *CtyunClientConfig {
  65. cfg.cpcfg = cpcfg
  66. return cfg
  67. }
  68. func (cfg *CtyunClientConfig) Debug(debug bool) *CtyunClientConfig {
  69. cfg.debug = debug
  70. return cfg
  71. }
  72. type SCtyunClient struct {
  73. *CtyunClientConfig
  74. regions []SRegion
  75. lock sync.Mutex
  76. client *http.Client
  77. ctx context.Context
  78. }
  79. func NewSCtyunClient(cfg *CtyunClientConfig) (*SCtyunClient, error) {
  80. client := &SCtyunClient{
  81. CtyunClientConfig: cfg,
  82. ctx: context.Background(),
  83. }
  84. client.ctx = context.WithValue(client.ctx, "time", time.Now())
  85. var err error
  86. client.regions, err = client.GetRegions()
  87. if err != nil {
  88. return nil, err
  89. }
  90. return client, nil
  91. }
  92. func (self *SCtyunClient) getUrl(service, resource string) (string, error) {
  93. switch service {
  94. case SERVICE_ECS, SERVICE_VPC, SERVICE_IMAGE:
  95. return fmt.Sprintf("https://ct%s-global.ctapi.ctyun.cn/%s", service, strings.TrimPrefix(resource, "/")), nil
  96. case SERVICE_ACCT, SERVICE_EBS:
  97. return fmt.Sprintf("https://%s-global.ctapi.ctyun.cn/%s", service, strings.TrimPrefix(resource, "/")), nil
  98. default:
  99. return "", errors.Wrapf(cloudprovider.ErrNotSupported, "service %s", service)
  100. }
  101. }
  102. type sCtyunError struct {
  103. StatusCode string
  104. Code string
  105. EopErrCode string
  106. RequestId string
  107. Message string
  108. }
  109. func (self *sCtyunError) Error() string {
  110. return jsonutils.Marshal(self).String()
  111. }
  112. func (self *sCtyunError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
  113. if body != nil {
  114. body.Unmarshal(self)
  115. }
  116. if strings.Contains(self.Message, "signature verification failed") {
  117. return errors.Wrapf(cloudprovider.ErrInvalidAccessKey, "%s", jsonutils.Marshal(self).String())
  118. }
  119. return self
  120. }
  121. func (cli *SCtyunClient) getDefaultClient() *http.Client {
  122. cli.lock.Lock()
  123. defer cli.lock.Unlock()
  124. if !gotypes.IsNil(cli.client) {
  125. return cli.client
  126. }
  127. cli.client = httputils.GetTimeoutClient(time.Minute)
  128. httputils.SetClientProxyFunc(cli.client, cli.cpcfg.ProxyFunc)
  129. ts, _ := cli.client.Transport.(*http.Transport)
  130. ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
  131. cli.client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) {
  132. if req.Method == "GET" {
  133. return nil, nil
  134. }
  135. for _, prefix := range []string{"list", "query", "info", "get", "detail", "show"} {
  136. if strings.Contains(req.URL.Path, prefix) {
  137. return nil, nil
  138. }
  139. }
  140. if cli.cpcfg.ReadOnly {
  141. return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
  142. }
  143. return nil, nil
  144. })
  145. return cli.client
  146. }
  147. func (self *SCtyunClient) sign(req *http.Request) (string, error) {
  148. eopDate := req.Header.Get("eop-date")
  149. requestId := req.Header.Get("ctyun-eop-request-id")
  150. headerStr := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n",
  151. requestId,
  152. eopDate,
  153. )
  154. keys := []string{}
  155. for key := range req.URL.Query() {
  156. keys = append(keys, key)
  157. }
  158. sort.Strings(keys)
  159. parts := []string{}
  160. for _, key := range keys {
  161. parts = append(parts, fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(req.URL.Query().Get(key))))
  162. }
  163. body := []byte{}
  164. if req.Method == "POST" {
  165. var err error
  166. body, err = ioutil.ReadAll(req.Body)
  167. if err != nil {
  168. return "", errors.Wrapf(err, "read body")
  169. }
  170. req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
  171. }
  172. var hmacSha256 = func(secret, data []byte) []byte {
  173. hasher := hmac.New(sha256.New, []byte(secret))
  174. hasher.Write(data)
  175. return hasher.Sum(nil)
  176. }
  177. hash := sha256.New()
  178. hash.Write(body)
  179. bodyHash := hex.EncodeToString(hash.Sum(nil))
  180. signStr := fmt.Sprintf("%s\n%s\n%s", headerStr, strings.Join(parts, "&"), bodyHash)
  181. kTime := hmacSha256([]byte(self.accessSecret), []byte(eopDate))
  182. kAk := hmacSha256(kTime, []byte(self.accessKey))
  183. t := strings.Split(eopDate, "T")[0]
  184. kDate := hmacSha256(kAk, []byte(t))
  185. signBase64 := base64.StdEncoding.EncodeToString(hmacSha256(kDate, []byte(signStr)))
  186. return fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", self.accessKey, signBase64), nil
  187. }
  188. func (self *SCtyunClient) Do(req *http.Request) (*http.Response, error) {
  189. client := self.getDefaultClient()
  190. req.Header.Set("Content-Type", "application/json")
  191. req.Header.Set("ctyun-eop-request-id", utils.GenRequestId(20))
  192. sh, _ := time.LoadLocation("Asia/Shanghai")
  193. req.Header.Set("eop-date", time.Now().In(sh).Format("20060102T150405Z"))
  194. signature, err := self.sign(req)
  195. if err != nil {
  196. return nil, errors.Wrapf(err, "sign")
  197. }
  198. req.Header.Set("Eop-Authorization", signature)
  199. return client.Do(req)
  200. }
  201. func (self *SCtyunClient) list(service, resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  202. return self.request(httputils.GET, service, resource, params)
  203. }
  204. func (self *SCtyunClient) GetRegions() ([]SRegion, error) {
  205. resp, err := self.list(SERVICE_ECS, "/v4/region/list-regions", nil)
  206. if err != nil {
  207. return nil, err
  208. }
  209. ret := struct {
  210. ReturnObj struct {
  211. RegionList []SRegion
  212. }
  213. }{}
  214. err = resp.Unmarshal(&ret)
  215. if err != nil {
  216. return nil, err
  217. }
  218. for i := range ret.ReturnObj.RegionList {
  219. ret.ReturnObj.RegionList[i].client = self
  220. }
  221. return ret.ReturnObj.RegionList, nil
  222. }
  223. func (self *SCtyunClient) post(service, resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  224. return self.request(httputils.POST, service, resource, params)
  225. }
  226. func (self *SCtyunClient) request(method httputils.THttpMethod, service, resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
  227. uri, err := self.getUrl(service, resource)
  228. if err != nil {
  229. return nil, err
  230. }
  231. if params == nil {
  232. params = map[string]interface{}{}
  233. }
  234. var body jsonutils.JSONObject = nil
  235. switch method {
  236. case httputils.GET:
  237. values := url.Values{}
  238. for k, v := range params {
  239. value := ""
  240. switch v.(type) {
  241. case string:
  242. value = fmt.Sprintf("%s", v)
  243. case int:
  244. value = fmt.Sprintf("%d", v)
  245. default:
  246. value = fmt.Sprintf("%d", v)
  247. }
  248. values.Set(k, value)
  249. }
  250. if len(params) > 0 {
  251. uri = fmt.Sprintf("%s?%s", uri, values.Encode())
  252. }
  253. case httputils.POST:
  254. body = jsonutils.Marshal(params)
  255. }
  256. req := httputils.NewJsonRequest(method, uri, body)
  257. ctErr := &sCtyunError{}
  258. client := httputils.NewJsonClient(self)
  259. _, resp, err := client.Send(self.ctx, req, ctErr, self.debug)
  260. if err != nil {
  261. return nil, err
  262. }
  263. ret := struct {
  264. Message string
  265. Description string
  266. StatusCode int
  267. ErrorCode string
  268. }{}
  269. err = resp.Unmarshal(&ret)
  270. if err != nil {
  271. return nil, errors.Wrapf(err, "Unmarshal")
  272. }
  273. if ret.StatusCode == 800 || (ret.StatusCode == 900 && resp.Contains("returnObj")) {
  274. return resp, nil
  275. }
  276. if strings.HasSuffix(ret.ErrorCode, "NotFound") || ret.ErrorCode == "ebs.ebsInfo.get volume resourceId failed" {
  277. return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", resp.String())
  278. }
  279. log.Errorf("request %s with params %s error: %s", uri, jsonutils.Marshal(body).String(), resp.String())
  280. return nil, fmt.Errorf("%s", resp.String())
  281. }
  282. func (self *SCtyunClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) {
  283. ret := []cloudprovider.ICloudRegion{}
  284. for i := range self.regions {
  285. if !self.regions[i].OpenapiAvailable {
  286. continue
  287. }
  288. self.regions[i].client = self
  289. ret = append(ret, &self.regions[i])
  290. }
  291. return ret, nil
  292. }
  293. func (self *SCtyunClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
  294. subAccounts := make([]cloudprovider.SSubAccount, 0)
  295. subAccount := cloudprovider.SSubAccount{}
  296. subAccount.Id = self.GetAccountId()
  297. subAccount.Name = self.cpcfg.Name
  298. subAccount.Account = self.accessKey
  299. subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
  300. subAccounts = append(subAccounts, subAccount)
  301. return subAccounts, nil
  302. }
  303. func (client *SCtyunClient) GetAccountId() string {
  304. return client.accessKey
  305. }
  306. func (self *SCtyunClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
  307. region, err := self.GetRegion(id)
  308. if err != nil {
  309. return nil, err
  310. }
  311. return region, nil
  312. }
  313. func (self *SCtyunClient) GetIProjects() ([]cloudprovider.ICloudProject, error) {
  314. return []cloudprovider.ICloudProject{}, nil
  315. }
  316. func (self *SCtyunClient) GetAccessEnv() string {
  317. return api.CLOUD_ACCESS_ENV_CTYUN_CHINA
  318. }
  319. func (self *SCtyunClient) GetRegion(id string) (*SRegion, error) {
  320. for i := range self.regions {
  321. self.regions[i].client = self
  322. if self.regions[i].GetId() == id || self.regions[i].GetGlobalId() == id || self.regions[i].RegionId == id {
  323. return &self.regions[i], nil
  324. }
  325. }
  326. return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", id)
  327. }
  328. func (self *SCtyunClient) GetCloudRegionExternalIdPrefix() string {
  329. return CLOUD_PROVIDER_CTYUN
  330. }
  331. func (self *SCtyunClient) GetCapabilities() []string {
  332. caps := []string{
  333. cloudprovider.CLOUD_CAPABILITY_COMPUTE,
  334. cloudprovider.CLOUD_CAPABILITY_NETWORK,
  335. cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP,
  336. cloudprovider.CLOUD_CAPABILITY_EIP,
  337. }
  338. return caps
  339. }
  340. func (self *SCtyunClient) GetBalance() {
  341. _, err := self.post(SERVICE_ACCT, "/bill_queryBalance", nil)
  342. if err != nil {
  343. return
  344. }
  345. }