| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- // Copyright 2019 Yunion
- //
- // Licensed under the Apache License, Version 2.0 (the "License");
- // you may not use this file except in compliance with the License.
- // You may obtain a copy of the License at
- //
- // http://www.apache.org/licenses/LICENSE-2.0
- //
- // Unless required by applicable law or agreed to in writing, software
- // distributed under the License is distributed on an "AS IS" BASIS,
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- // See the License for the specific language governing permissions and
- // limitations under the License.
- package ctyun
- import (
- "bytes"
- "context"
- "crypto/hmac"
- "crypto/sha256"
- "crypto/tls"
- "encoding/base64"
- "encoding/hex"
- "fmt"
- "io/ioutil"
- "net/http"
- "net/url"
- "sort"
- "strings"
- "sync"
- "time"
- "yunion.io/x/jsonutils"
- "yunion.io/x/log"
- "yunion.io/x/pkg/errors"
- "yunion.io/x/pkg/gotypes"
- "yunion.io/x/pkg/util/httputils"
- "yunion.io/x/pkg/utils"
- api "yunion.io/x/cloudmux/pkg/apis/compute"
- "yunion.io/x/cloudmux/pkg/cloudprovider"
- )
- const (
- CLOUD_PROVIDER_CTYUN = api.CLOUD_PROVIDER_CTYUN
- CLOUD_PROVIDER_CTYUN_CN = "天翼云"
- CLOUD_PROVIDER_CTYUN_EN = CLOUD_PROVIDER_CTYUN
- SERVICE_ECS = "ecs"
- SERVICE_VPC = "vpc"
- SERVICE_IMAGE = "image"
- SERVICE_ACCT = "acct"
- SERVICE_EBS = "ebs"
- )
- type CtyunClientConfig struct {
- cpcfg cloudprovider.ProviderConfig
- projectId string
- accessKey string
- accessSecret string
- debug bool
- }
- func NewSCtyunClientConfig(accessKey, accessSecret string) *CtyunClientConfig {
- cfg := &CtyunClientConfig{
- accessKey: accessKey,
- accessSecret: accessSecret,
- }
- return cfg
- }
- func (cfg *CtyunClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *CtyunClientConfig {
- cfg.cpcfg = cpcfg
- return cfg
- }
- func (cfg *CtyunClientConfig) Debug(debug bool) *CtyunClientConfig {
- cfg.debug = debug
- return cfg
- }
- type SCtyunClient struct {
- *CtyunClientConfig
- regions []SRegion
- lock sync.Mutex
- client *http.Client
- ctx context.Context
- }
- func NewSCtyunClient(cfg *CtyunClientConfig) (*SCtyunClient, error) {
- client := &SCtyunClient{
- CtyunClientConfig: cfg,
- ctx: context.Background(),
- }
- client.ctx = context.WithValue(client.ctx, "time", time.Now())
- var err error
- client.regions, err = client.GetRegions()
- if err != nil {
- return nil, err
- }
- return client, nil
- }
- func (self *SCtyunClient) getUrl(service, resource string) (string, error) {
- switch service {
- case SERVICE_ECS, SERVICE_VPC, SERVICE_IMAGE:
- return fmt.Sprintf("https://ct%s-global.ctapi.ctyun.cn/%s", service, strings.TrimPrefix(resource, "/")), nil
- case SERVICE_ACCT, SERVICE_EBS:
- return fmt.Sprintf("https://%s-global.ctapi.ctyun.cn/%s", service, strings.TrimPrefix(resource, "/")), nil
- default:
- return "", errors.Wrapf(cloudprovider.ErrNotSupported, "service %s", service)
- }
- }
- type sCtyunError struct {
- StatusCode string
- Code string
- EopErrCode string
- RequestId string
- Message string
- }
- func (self *sCtyunError) Error() string {
- return jsonutils.Marshal(self).String()
- }
- func (self *sCtyunError) ParseErrorFromJsonResponse(statusCode int, status string, body jsonutils.JSONObject) error {
- if body != nil {
- body.Unmarshal(self)
- }
- if strings.Contains(self.Message, "signature verification failed") {
- return errors.Wrapf(cloudprovider.ErrInvalidAccessKey, "%s", jsonutils.Marshal(self).String())
- }
- return self
- }
- func (cli *SCtyunClient) getDefaultClient() *http.Client {
- cli.lock.Lock()
- defer cli.lock.Unlock()
- if !gotypes.IsNil(cli.client) {
- return cli.client
- }
- cli.client = httputils.GetTimeoutClient(time.Minute)
- httputils.SetClientProxyFunc(cli.client, cli.cpcfg.ProxyFunc)
- ts, _ := cli.client.Transport.(*http.Transport)
- ts.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
- cli.client.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response) error, error) {
- if req.Method == "GET" {
- return nil, nil
- }
- for _, prefix := range []string{"list", "query", "info", "get", "detail", "show"} {
- if strings.Contains(req.URL.Path, prefix) {
- return nil, nil
- }
- }
- if cli.cpcfg.ReadOnly {
- return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
- }
- return nil, nil
- })
- return cli.client
- }
- func (self *SCtyunClient) sign(req *http.Request) (string, error) {
- eopDate := req.Header.Get("eop-date")
- requestId := req.Header.Get("ctyun-eop-request-id")
- headerStr := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n",
- requestId,
- eopDate,
- )
- keys := []string{}
- for key := range req.URL.Query() {
- keys = append(keys, key)
- }
- sort.Strings(keys)
- parts := []string{}
- for _, key := range keys {
- parts = append(parts, fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(req.URL.Query().Get(key))))
- }
- body := []byte{}
- if req.Method == "POST" {
- var err error
- body, err = ioutil.ReadAll(req.Body)
- if err != nil {
- return "", errors.Wrapf(err, "read body")
- }
- req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
- }
- var hmacSha256 = func(secret, data []byte) []byte {
- hasher := hmac.New(sha256.New, []byte(secret))
- hasher.Write(data)
- return hasher.Sum(nil)
- }
- hash := sha256.New()
- hash.Write(body)
- bodyHash := hex.EncodeToString(hash.Sum(nil))
- signStr := fmt.Sprintf("%s\n%s\n%s", headerStr, strings.Join(parts, "&"), bodyHash)
- kTime := hmacSha256([]byte(self.accessSecret), []byte(eopDate))
- kAk := hmacSha256(kTime, []byte(self.accessKey))
- t := strings.Split(eopDate, "T")[0]
- kDate := hmacSha256(kAk, []byte(t))
- signBase64 := base64.StdEncoding.EncodeToString(hmacSha256(kDate, []byte(signStr)))
- return fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", self.accessKey, signBase64), nil
- }
- func (self *SCtyunClient) Do(req *http.Request) (*http.Response, error) {
- client := self.getDefaultClient()
- req.Header.Set("Content-Type", "application/json")
- req.Header.Set("ctyun-eop-request-id", utils.GenRequestId(20))
- sh, _ := time.LoadLocation("Asia/Shanghai")
- req.Header.Set("eop-date", time.Now().In(sh).Format("20060102T150405Z"))
- signature, err := self.sign(req)
- if err != nil {
- return nil, errors.Wrapf(err, "sign")
- }
- req.Header.Set("Eop-Authorization", signature)
- return client.Do(req)
- }
- func (self *SCtyunClient) list(service, resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
- return self.request(httputils.GET, service, resource, params)
- }
- func (self *SCtyunClient) GetRegions() ([]SRegion, error) {
- resp, err := self.list(SERVICE_ECS, "/v4/region/list-regions", nil)
- if err != nil {
- return nil, err
- }
- ret := struct {
- ReturnObj struct {
- RegionList []SRegion
- }
- }{}
- err = resp.Unmarshal(&ret)
- if err != nil {
- return nil, err
- }
- for i := range ret.ReturnObj.RegionList {
- ret.ReturnObj.RegionList[i].client = self
- }
- return ret.ReturnObj.RegionList, nil
- }
- func (self *SCtyunClient) post(service, resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
- return self.request(httputils.POST, service, resource, params)
- }
- func (self *SCtyunClient) request(method httputils.THttpMethod, service, resource string, params map[string]interface{}) (jsonutils.JSONObject, error) {
- uri, err := self.getUrl(service, resource)
- if err != nil {
- return nil, err
- }
- if params == nil {
- params = map[string]interface{}{}
- }
- var body jsonutils.JSONObject = nil
- switch method {
- case httputils.GET:
- values := url.Values{}
- for k, v := range params {
- value := ""
- switch v.(type) {
- case string:
- value = fmt.Sprintf("%s", v)
- case int:
- value = fmt.Sprintf("%d", v)
- default:
- value = fmt.Sprintf("%d", v)
- }
- values.Set(k, value)
- }
- if len(params) > 0 {
- uri = fmt.Sprintf("%s?%s", uri, values.Encode())
- }
- case httputils.POST:
- body = jsonutils.Marshal(params)
- }
- req := httputils.NewJsonRequest(method, uri, body)
- ctErr := &sCtyunError{}
- client := httputils.NewJsonClient(self)
- _, resp, err := client.Send(self.ctx, req, ctErr, self.debug)
- if err != nil {
- return nil, err
- }
- ret := struct {
- Message string
- Description string
- StatusCode int
- ErrorCode string
- }{}
- err = resp.Unmarshal(&ret)
- if err != nil {
- return nil, errors.Wrapf(err, "Unmarshal")
- }
- if ret.StatusCode == 800 || (ret.StatusCode == 900 && resp.Contains("returnObj")) {
- return resp, nil
- }
- if strings.HasSuffix(ret.ErrorCode, "NotFound") || ret.ErrorCode == "ebs.ebsInfo.get volume resourceId failed" {
- return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", resp.String())
- }
- log.Errorf("request %s with params %s error: %s", uri, jsonutils.Marshal(body).String(), resp.String())
- return nil, fmt.Errorf("%s", resp.String())
- }
- func (self *SCtyunClient) GetIRegions() ([]cloudprovider.ICloudRegion, error) {
- ret := []cloudprovider.ICloudRegion{}
- for i := range self.regions {
- if !self.regions[i].OpenapiAvailable {
- continue
- }
- self.regions[i].client = self
- ret = append(ret, &self.regions[i])
- }
- return ret, nil
- }
- func (self *SCtyunClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
- subAccounts := make([]cloudprovider.SSubAccount, 0)
- subAccount := cloudprovider.SSubAccount{}
- subAccount.Id = self.GetAccountId()
- subAccount.Name = self.cpcfg.Name
- subAccount.Account = self.accessKey
- subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
- subAccounts = append(subAccounts, subAccount)
- return subAccounts, nil
- }
- func (client *SCtyunClient) GetAccountId() string {
- return client.accessKey
- }
- func (self *SCtyunClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
- region, err := self.GetRegion(id)
- if err != nil {
- return nil, err
- }
- return region, nil
- }
- func (self *SCtyunClient) GetIProjects() ([]cloudprovider.ICloudProject, error) {
- return []cloudprovider.ICloudProject{}, nil
- }
- func (self *SCtyunClient) GetAccessEnv() string {
- return api.CLOUD_ACCESS_ENV_CTYUN_CHINA
- }
- func (self *SCtyunClient) GetRegion(id string) (*SRegion, error) {
- for i := range self.regions {
- self.regions[i].client = self
- if self.regions[i].GetId() == id || self.regions[i].GetGlobalId() == id || self.regions[i].RegionId == id {
- return &self.regions[i], nil
- }
- }
- return nil, errors.Wrapf(cloudprovider.ErrNotFound, "%s", id)
- }
- func (self *SCtyunClient) GetCloudRegionExternalIdPrefix() string {
- return CLOUD_PROVIDER_CTYUN
- }
- func (self *SCtyunClient) GetCapabilities() []string {
- caps := []string{
- cloudprovider.CLOUD_CAPABILITY_COMPUTE,
- cloudprovider.CLOUD_CAPABILITY_NETWORK,
- cloudprovider.CLOUD_CAPABILITY_SECURITY_GROUP,
- cloudprovider.CLOUD_CAPABILITY_EIP,
- }
- return caps
- }
- func (self *SCtyunClient) GetBalance() {
- _, err := self.post(SERVICE_ACCT, "/bill_queryBalance", nil)
- if err != nil {
- return
- }
- }
|