yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/ctyun/ctyun.go (about)

     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  
    15  package ctyun
    16  
    17  import (
    18  	"context"
    19  	"crypto/hmac"
    20  	"crypto/sha1"
    21  	"encoding/base64"
    22  	"fmt"
    23  	"net/http"
    24  	"net/url"
    25  	"strconv"
    26  	"strings"
    27  	"time"
    28  
    29  	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/utils"
    30  
    31  	"yunion.io/x/jsonutils"
    32  	"yunion.io/x/pkg/errors"
    33  
    34  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    35  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    36  	"yunion.io/x/onecloud/pkg/util/httputils"
    37  )
    38  
    39  const (
    40  	CTYUN_API_HOST          = "https://api.ctyun.cn"
    41  	CLOUD_PROVIDER_CTYUN    = api.CLOUD_PROVIDER_CTYUN
    42  	CLOUD_PROVIDER_CTYUN_CN = "天翼云"
    43  	CLOUD_PROVIDER_CTYUN_EN = "Ctyun"
    44  	CTYUN_DEFAULT_REGION    = "cn-bj4"
    45  
    46  	CTYUN_API_VERSION = "2019-11-22"
    47  )
    48  
    49  type CtyunClientConfig struct {
    50  	cpcfg   cloudprovider.ProviderConfig
    51  	options *cloudprovider.SCtyunExtraOptions
    52  
    53  	projectId    string
    54  	accessKey    string
    55  	accessSecret string
    56  
    57  	debug bool
    58  }
    59  
    60  func NewSCtyunClientConfig(accessKey, accessSecret string, options *cloudprovider.SCtyunExtraOptions) *CtyunClientConfig {
    61  	cfg := &CtyunClientConfig{
    62  		accessKey:    accessKey,
    63  		accessSecret: accessSecret,
    64  		options:      options,
    65  	}
    66  	return cfg
    67  }
    68  
    69  func (cfg *CtyunClientConfig) ProjectId(projectId string) *CtyunClientConfig {
    70  	cfg.projectId = projectId
    71  	return cfg
    72  }
    73  
    74  func (cfg *CtyunClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *CtyunClientConfig {
    75  	cfg.cpcfg = cpcfg
    76  	return cfg
    77  }
    78  
    79  func (cfg *CtyunClientConfig) Debug(debug bool) *CtyunClientConfig {
    80  	cfg.debug = debug
    81  	return cfg
    82  }
    83  
    84  type SCtyunClient struct {
    85  	*CtyunClientConfig
    86  
    87  	httpClient *http.Client
    88  	iregions   []cloudprovider.ICloudRegion
    89  }
    90  
    91  func NewSCtyunClient(cfg *CtyunClientConfig) (*SCtyunClient, error) {
    92  	httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient()
    93  	ts, _ := httpClient.Transport.(*http.Transport)
    94  	httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) {
    95  		if cfg.cpcfg.ReadOnly {
    96  			if req.Method == "GET" {
    97  				return nil, nil
    98  			}
    99  			return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
   100  		}
   101  		return nil, nil
   102  	})
   103  	client := &SCtyunClient{
   104  		CtyunClientConfig: cfg,
   105  		httpClient:        httpClient,
   106  	}
   107  
   108  	err := client.init()
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  
   113  	return client, nil
   114  }
   115  
   116  func (client *SCtyunClient) init() error {
   117  	err := client.fetchRegions()
   118  	if err != nil {
   119  		return err
   120  	}
   121  
   122  	return nil
   123  }
   124  
   125  func (client *SCtyunClient) fetchRegions() error {
   126  	resp, err := client.DoGet("/apiproxy/v3/order/getZoneConfig", map[string]string{})
   127  	if err != nil {
   128  		return err
   129  	}
   130  
   131  	zones := []SZone{}
   132  	err = resp.Unmarshal(&zones, "returnObj")
   133  	if err != nil {
   134  		return err
   135  	}
   136  
   137  	regions := map[string]SRegion{}
   138  	for i := range zones {
   139  		zone := zones[i]
   140  
   141  		if len(client.projectId) > 0 && !strings.Contains(client.projectId, zone.RegionID) {
   142  			continue
   143  		}
   144  
   145  		if region, ok := regions[zone.RegionID]; !ok {
   146  			region = SRegion{
   147  				client:         client,
   148  				Description:    zone.ZoneName,
   149  				ID:             zone.RegionID,
   150  				ParentRegionID: zone.RegionID,
   151  				RegionName:     zone.ZoneName,
   152  				izones:         []cloudprovider.ICloudZone{&zone},
   153  			}
   154  
   155  			zone.region = &region
   156  			zone.host = &SHost{zone: &zone}
   157  			regions[zone.RegionID] = region
   158  		} else {
   159  			zone.region = &region
   160  			zone.host = &SHost{zone: &zone}
   161  			region.izones = append(region.izones, &zone)
   162  		}
   163  	}
   164  
   165  	client.iregions = []cloudprovider.ICloudRegion{}
   166  	for k := range regions {
   167  		region := regions[k]
   168  		client.iregions = append(client.iregions, &region)
   169  	}
   170  	return nil
   171  }
   172  
   173  func (client *SCtyunClient) DoGet(apiName string, queries map[string]string) (jsonutils.JSONObject, error) {
   174  	return formRequest(client, httputils.GET, apiName, queries, nil)
   175  }
   176  
   177  func (client *SCtyunClient) DoPost(apiName string, params map[string]jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   178  	return formRequest(client, httputils.POST, apiName, nil, params)
   179  }
   180  
   181  func getCustiomInfo(t string, crmBizId string, accountId string) jsonutils.JSONObject {
   182  	if len(t) == 0 {
   183  		return nil
   184  	}
   185  
   186  	customeInfo := jsonutils.NewDict()
   187  	//customeInfo.Set("name", jsonutils.NewString(""))
   188  	//customeInfo.Set("email", jsonutils.NewString(""))
   189  	//customeInfo.Set("phone", jsonutils.NewString(""))
   190  	indentity := jsonutils.NewDict()
   191  	if len(crmBizId) > 0 {
   192  		indentity.Set("crmBizId", jsonutils.NewString(crmBizId))
   193  	}
   194  
   195  	if len(accountId) > 0 {
   196  		indentity.Set("accountId", jsonutils.NewString(accountId))
   197  	}
   198  
   199  	if len(t) > 0 {
   200  		customeInfo.Set("type", jsonutils.NewString(t))
   201  		customeInfo.Set("identity", indentity)
   202  	}
   203  
   204  	return customeInfo
   205  }
   206  
   207  func formRequest(client *SCtyunClient, method httputils.THttpMethod, apiName string, queries map[string]string, params map[string]jsonutils.JSONObject) (jsonutils.JSONObject, error) {
   208  	header := http.Header{}
   209  	// signer
   210  	{
   211  		content := []string{}
   212  		for k, v := range queries {
   213  			content = append(content, v)
   214  			header.Set(k, v)
   215  		}
   216  
   217  		for _, v := range params {
   218  			c, _ := v.GetString()
   219  			content = append(content, c)
   220  		}
   221  
   222  		// contentMd5 := fmt.Sprintf("%x", md5.Sum([]byte(strings.Join(content, "\n"))))
   223  		// contentMd5 = base64.StdEncoding.EncodeToString([]byte(contentMd5))
   224  		contentRaw := strings.Join(content, "\n")
   225  		contentMd5 := utils.GetMD5Base64([]byte(contentRaw))
   226  
   227  		// EEE, d MMM yyyy HH:mm:ss z
   228  		// Mon, 2 Jan 2006 15:04:05 MST
   229  		requestDate := time.Now().Format("Mon, 2 Jan 2006 15:04:05 MST")
   230  		hashMac := hmac.New(sha1.New, []byte(client.accessSecret))
   231  		hashRawString := strings.Join([]string{contentMd5, requestDate, apiName}, "\n")
   232  		hashMac.Write([]byte(hashRawString))
   233  		hsum := base64.StdEncoding.EncodeToString(hashMac.Sum(nil))
   234  
   235  		header.Set("accessKey", client.accessKey)
   236  		header.Set("contentMD5", contentMd5)
   237  		header.Set("requestDate", requestDate)
   238  		header.Set("hmac", hsum)
   239  		// 平台类型,整数类型,取值范围:2或3,传2表示2.0自营资源,传3表示3.0合营资源,该参数不需要加密。
   240  		header.Set("platform", "3")
   241  		// crm账号需要设置customInfo。目前发现只有VPC列表查询需要用到这个参数,其他的接口还没有发现此要求。为了简便对于crm 统一设置此项
   242  		if client.options != nil && len(client.options.CrmBizId) > 0 {
   243  			customInfo := getCustiomInfo("1", client.options.CrmBizId, "")
   244  			header.Set("customInfo", customInfo.String())
   245  		}
   246  	}
   247  
   248  	var reqbody string
   249  	ioData := strings.NewReader("")
   250  	if method == httputils.GET {
   251  		for k, v := range queries {
   252  			header.Set(k, v)
   253  		}
   254  	} else {
   255  		datas := url.Values{}
   256  		for k, v := range params {
   257  			c, _ := v.GetString()
   258  			datas.Add(k, c)
   259  		}
   260  
   261  		reqbody = datas.Encode()
   262  		ioData = strings.NewReader(reqbody)
   263  	}
   264  
   265  	header.Set("Content-Length", strconv.FormatInt(int64(ioData.Len()), 10))
   266  	header.Set("Content-Type", "application/x-www-form-urlencoded")
   267  
   268  	ctx := context.Background()
   269  	MAX_RETRY := 3
   270  	retry := 0
   271  
   272  	var err error
   273  	for retry < MAX_RETRY {
   274  		resp, err := httputils.Request(
   275  			client.httpClient,
   276  			ctx,
   277  			method,
   278  			CTYUN_API_HOST+apiName,
   279  			header,
   280  			ioData,
   281  			client.debug)
   282  
   283  		_, jsonResp, err := httputils.ParseJSONResponse(reqbody, resp, err, client.debug)
   284  		if err == nil {
   285  			if code, _ := jsonResp.Int("statusCode"); code != 800 {
   286  				if strings.Contains(jsonResp.String(), "NotFound") {
   287  					return nil, cloudprovider.ErrNotFound
   288  				}
   289  
   290  				return nil, &httputils.JSONClientError{Code: 400, Details: jsonResp.String()}
   291  			}
   292  
   293  			return jsonResp, nil
   294  		}
   295  
   296  		switch e := err.(type) {
   297  		case *httputils.JSONClientError:
   298  			if e.Code >= 499 {
   299  				time.Sleep(3 * time.Second)
   300  				retry += 1
   301  				continue
   302  			} else {
   303  				return nil, err
   304  			}
   305  		default:
   306  			return nil, err
   307  		}
   308  	}
   309  
   310  	return nil, fmt.Errorf("timeout for request: %s \n\n with params: %s", err, params)
   311  }
   312  
   313  func (self *SCtyunClient) GetIRegions() []cloudprovider.ICloudRegion {
   314  	return self.iregions
   315  }
   316  
   317  func (self *SCtyunClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
   318  	subAccounts := make([]cloudprovider.SSubAccount, 0)
   319  	for i := range self.iregions {
   320  		iregion := self.iregions[i]
   321  
   322  		s := cloudprovider.SSubAccount{
   323  			Name:         fmt.Sprintf("%s-%s", self.cpcfg.Name, iregion.GetId()),
   324  			Account:      fmt.Sprintf("%s/%s", self.accessKey, iregion.GetId()),
   325  			HealthStatus: api.CLOUD_PROVIDER_HEALTH_NORMAL,
   326  		}
   327  
   328  		subAccounts = append(subAccounts, s)
   329  	}
   330  
   331  	return subAccounts, nil
   332  }
   333  
   334  func (client *SCtyunClient) GetAccountId() string {
   335  	return client.accessKey
   336  }
   337  
   338  func (self *SCtyunClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
   339  	for i := 0; i < len(self.iregions); i += 1 {
   340  		if self.iregions[i].GetGlobalId() == id {
   341  			return self.iregions[i], nil
   342  		}
   343  	}
   344  
   345  	return nil, cloudprovider.ErrNotFound
   346  }
   347  
   348  func (self *SCtyunClient) GetIProjects() ([]cloudprovider.ICloudProject, error) {
   349  	return nil, cloudprovider.ErrNotImplemented
   350  }
   351  
   352  func (self *SCtyunClient) GetAccessEnv() string {
   353  	return api.CLOUD_ACCESS_ENV_CTYUN_CHINA
   354  }
   355  
   356  func (self *SCtyunClient) GetRegions() []SRegion {
   357  	regions := make([]SRegion, len(self.iregions))
   358  	for i := 0; i < len(regions); i += 1 {
   359  		region := self.iregions[i].(*SRegion)
   360  		regions[i] = *region
   361  	}
   362  	return regions
   363  }
   364  
   365  func (self *SCtyunClient) GetRegion(regionId string) *SRegion {
   366  	if len(regionId) == 0 {
   367  		regionId = CTYUN_DEFAULT_REGION
   368  	}
   369  	for i := 0; i < len(self.iregions); i += 1 {
   370  		if self.iregions[i].GetId() == regionId {
   371  			return self.iregions[i].(*SRegion)
   372  		}
   373  	}
   374  	return nil
   375  }
   376  
   377  func (self *SCtyunClient) GetCloudRegionExternalIdPrefix() string {
   378  	if len(self.projectId) > 0 {
   379  		return self.iregions[0].GetGlobalId()
   380  	} else {
   381  		return CLOUD_PROVIDER_CTYUN
   382  	}
   383  }
   384  
   385  func (self *SCtyunClient) GetCapabilities() []string {
   386  	caps := []string{
   387  		// cloudprovider.CLOUD_CAPABILITY_PROJECT,
   388  		cloudprovider.CLOUD_CAPABILITY_COMPUTE,
   389  		cloudprovider.CLOUD_CAPABILITY_NETWORK,
   390  		cloudprovider.CLOUD_CAPABILITY_EIP,
   391  		// cloudprovider.CLOUD_CAPABILITY_LOADBALANCER,
   392  		// cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE,
   393  		// cloudprovider.CLOUD_CAPABILITY_RDS,
   394  		// cloudprovider.CLOUD_CAPABILITY_CACHE,
   395  		// cloudprovider.CLOUD_CAPABILITY_EVENT,
   396  	}
   397  	return caps
   398  }