yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/apsara/apsara.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 apsara
    16  
    17  import (
    18  	"bytes"
    19  	"crypto/tls"
    20  	"fmt"
    21  	"io/ioutil"
    22  	"net/http"
    23  	"net/url"
    24  	"strings"
    25  	"time"
    26  
    27  	"github.com/aliyun/alibaba-cloud-sdk-go/sdk"
    28  	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/auth/credentials"
    29  	"github.com/aliyun/alibaba-cloud-sdk-go/sdk/requests"
    30  	"github.com/aliyun/aliyun-oss-go-sdk/oss"
    31  	"github.com/pkg/errors"
    32  
    33  	"yunion.io/x/jsonutils"
    34  	"yunion.io/x/log"
    35  	v "yunion.io/x/pkg/util/version"
    36  	"yunion.io/x/pkg/utils"
    37  
    38  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    39  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    40  	"yunion.io/x/onecloud/pkg/util/httputils"
    41  )
    42  
    43  const (
    44  	CLOUD_PROVIDER_APSARA    = api.CLOUD_PROVIDER_APSARA
    45  	CLOUD_PROVIDER_APSARA_CN = "阿里云专有云"
    46  	CLOUD_PROVIDER_APSARA_EN = "Aliyun Apsara"
    47  
    48  	APSARA_API_VERSION     = "2014-05-26"
    49  	APSARA_API_VERSION_VPC = "2016-04-28"
    50  	APSARA_API_VERSION_LB  = "2014-05-15"
    51  	APSARA_API_VERSION_KVS = "2015-01-01"
    52  
    53  	APSARA_API_VERSION_TRIAL = "2017-12-04"
    54  
    55  	APSARA_BSS_API_VERSION = "2017-12-14"
    56  
    57  	APSARA_RAM_API_VERSION  = "2015-05-01"
    58  	APSARA_API_VERION_RDS   = "2014-08-15"
    59  	APSARA_ASCM_API_VERSION = "2019-05-10"
    60  	APSARA_STS_API_VERSION  = "2015-04-01"
    61  	APSARA_OTS_API_VERSION  = "2016-06-20"
    62  
    63  	APSARA_PRODUCT_METRICS      = "Cms"
    64  	APSARA_PRODUCT_RDS          = "Rds"
    65  	APSARA_PRODUCT_VPC          = "Vpc"
    66  	APSARA_PRODUCT_KVSTORE      = "R-kvstore"
    67  	APSARA_PRODUCT_SLB          = "Slb"
    68  	APSARA_PRODUCT_ECS          = "Ecs"
    69  	APSARA_PRODUCT_ACTION_TRIAL = "actiontrail"
    70  	APSARA_PRODUCT_STS          = "Sts"
    71  	APSARA_PRODUCT_RAM          = "Ram"
    72  	APSARA_PRODUCT_ASCM         = "ascm"
    73  	APSARA_PRODUCT_OTS          = "ots"
    74  )
    75  
    76  type ApsaraClientConfig struct {
    77  	cpcfg          cloudprovider.ProviderConfig
    78  	accessKey      string
    79  	accessSecret   string
    80  	organizationId string
    81  	debug          bool
    82  }
    83  
    84  func NewApsaraClientConfig(accessKey, accessSecret string, endpoint string) *ApsaraClientConfig {
    85  	cfg := &ApsaraClientConfig{
    86  		accessKey:      accessKey,
    87  		accessSecret:   accessSecret,
    88  		organizationId: "1",
    89  	}
    90  	if info := strings.Split(accessKey, "/"); len(info) == 2 {
    91  		cfg.accessKey, cfg.organizationId = info[0], info[1]
    92  	}
    93  	return cfg
    94  }
    95  
    96  func (cfg *ApsaraClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *ApsaraClientConfig {
    97  	cfg.cpcfg = cpcfg
    98  	return cfg
    99  }
   100  
   101  func (cfg *ApsaraClientConfig) Debug(debug bool) *ApsaraClientConfig {
   102  	cfg.debug = debug
   103  	return cfg
   104  }
   105  
   106  func (cfg ApsaraClientConfig) Copy() ApsaraClientConfig {
   107  	return cfg
   108  }
   109  
   110  type SApsaraClient struct {
   111  	*ApsaraClientConfig
   112  
   113  	ownerId   string
   114  	ownerName string
   115  
   116  	iregions []cloudprovider.ICloudRegion
   117  }
   118  
   119  func NewApsaraClient(cfg *ApsaraClientConfig) (*SApsaraClient, error) {
   120  	client := SApsaraClient{
   121  		ApsaraClientConfig: cfg,
   122  	}
   123  
   124  	err := client.fetchRegions()
   125  	if err != nil {
   126  		return nil, errors.Wrap(err, "fetchRegions")
   127  	}
   128  	return &client, nil
   129  }
   130  
   131  func (self *SApsaraClient) getDomain(product string) string {
   132  	return self.cpcfg.URL
   133  }
   134  
   135  func productRequest(client *sdk.Client, product, domain, apiVersion, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
   136  	params["Product"] = product
   137  	return jsonRequest(client, domain, apiVersion, apiName, params, debug)
   138  }
   139  
   140  func jsonRequest(client *sdk.Client, domain, apiVersion, apiName string, params map[string]string, debug bool) (jsonutils.JSONObject, error) {
   141  	if debug {
   142  		log.Debugf("request %s %s %s %s", domain, apiVersion, apiName, params)
   143  	}
   144  	var resp jsonutils.JSONObject
   145  	var err error
   146  	for i := 1; i < 4; i++ {
   147  		resp, err = _jsonRequest(client, domain, apiVersion, apiName, params)
   148  		retry := false
   149  		if err != nil {
   150  			for _, code := range []string{
   151  				"InvalidAccessKeyId.NotFound",
   152  			} {
   153  				if strings.Contains(err.Error(), code) {
   154  					return nil, err
   155  				}
   156  			}
   157  			for _, code := range []string{"404 Not Found", "EntityNotExist.Role", "EntityNotExist.Group"} {
   158  				if strings.Contains(err.Error(), code) {
   159  					return nil, errors.Wrapf(cloudprovider.ErrNotFound, err.Error())
   160  				}
   161  			}
   162  			for _, code := range []string{
   163  				"EOF",
   164  				"i/o timeout",
   165  				"TLS handshake timeout",
   166  				"connection reset by peer",
   167  				"server misbehaving",
   168  				"SignatureNonceUsed",
   169  				"InvalidInstance.NotSupported",
   170  				"try later",
   171  				"BackendServer.configuring",
   172  				"Another operation is being performed", //Another operation is being performed on the DB instance or the DB instance is faulty(赋予RDS账号权限)
   173  			} {
   174  				if strings.Contains(err.Error(), code) {
   175  					retry = true
   176  					break
   177  				}
   178  			}
   179  		}
   180  		if retry {
   181  			if debug {
   182  				log.Debugf("Retry %d...", i)
   183  			}
   184  			time.Sleep(time.Second * time.Duration(i*10))
   185  			continue
   186  		}
   187  		if debug {
   188  			log.Debugf("Response: %s", resp)
   189  		}
   190  		return resp, err
   191  	}
   192  	return resp, errors.Wrapf(err, "jsonRequest")
   193  }
   194  
   195  func _jsonRequest(client *sdk.Client, domain string, version string, apiName string, params map[string]string) (jsonutils.JSONObject, error) {
   196  	req := requests.NewCommonRequest()
   197  	req.Domain = domain
   198  	req.Version = version
   199  	req.ApiName = apiName
   200  	req.Scheme = "http"
   201  	req.Method = "POST"
   202  	id := ""
   203  	if params != nil {
   204  		for k, v := range params {
   205  			if strings.HasPrefix(k, "x-acs-") {
   206  				req.GetHeaders()[k] = v
   207  				continue
   208  			}
   209  			req.QueryParams[k] = v
   210  			if strings.ToLower(k) != "regionid" && strings.HasSuffix(k, "Id") {
   211  				id = v
   212  			}
   213  		}
   214  	}
   215  	req.GetHeaders()["User-Agent"] = "vendor/yunion-OneCloud@" + v.Get().GitVersion
   216  	if strings.HasPrefix(apiName, "Describe") && len(id) > 0 {
   217  		req.GetHeaders()["x-acs-instanceId"] = id
   218  	}
   219  
   220  	resp, err := processCommonRequest(client, req)
   221  	if err != nil {
   222  		return nil, errors.Wrapf(err, "processCommonRequest(%s, %s)", apiName, params)
   223  	}
   224  	body, err := jsonutils.Parse(resp.GetHttpContentBytes())
   225  	if err != nil {
   226  		return nil, errors.Wrapf(err, "jsonutils.Parse")
   227  	}
   228  	//{"Code":"InvalidInstanceType.ValueNotSupported","HostId":"ecs.apsaracs.com","Message":"The specified instanceType beyond the permitted range.","RequestId":"0042EE30-0EDF-48A7-A414-56229D4AD532"}
   229  	//{"Code":"200","Message":"successful","PageNumber":1,"PageSize":50,"RequestId":"BB4C970C-0E23-48DC-A3B0-EB21FFC70A29","RouterTableList":{"RouterTableListType":[{"CreationTime":"2017-03-19T13:37:40Z","Description":"","ResourceGroupId":"rg-acfmwie3cqoobmi","RouteTableId":"vtb-j6c60lectdi80rk5xz43g","RouteTableName":"","RouteTableType":"System","RouterId":"vrt-j6c00qrol733dg36iq4qj","RouterType":"VRouter","VSwitchIds":{"VSwitchId":["vsw-j6c3gig5ub4fmi2veyrus"]},"VpcId":"vpc-j6c86z3sh8ufhgsxwme0q"}]},"Success":true,"TotalCount":1}
   230  	if body.Contains("Code") {
   231  		code, _ := body.GetString("Code")
   232  		if len(code) > 0 && !utils.IsInStringArray(code, []string{"200"}) {
   233  			return nil, fmt.Errorf(body.String())
   234  		}
   235  	}
   236  	if body.Contains("errorKey") {
   237  		return nil, errors.Errorf(body.String())
   238  	}
   239  	return body, nil
   240  }
   241  
   242  func (self *SApsaraClient) getDefaultClient(regionId string) (*sdk.Client, error) {
   243  	if len(self.iregions) > 0 && len(regionId) == 0 {
   244  		regionId = self.iregions[0].GetId()
   245  	}
   246  	transport := httputils.GetTransport(true)
   247  	transport.Proxy = self.cpcfg.ProxyFunc
   248  	transport.TLSClientConfig = &tls.Config{InsecureSkipVerify: true}
   249  	client, err := sdk.NewClientWithOptions(
   250  		regionId,
   251  		&sdk.Config{
   252  			HttpTransport: transport,
   253  			Transport: cloudprovider.GetCheckTransport(transport, func(req *http.Request) (func(resp *http.Response), error) {
   254  				params, err := url.ParseQuery(req.URL.RawQuery)
   255  				if err != nil {
   256  					return nil, errors.Wrapf(err, "ParseQuery(%s)", req.URL.RawQuery)
   257  				}
   258  				action := params.Get("Action")
   259  				service := strings.ToLower(params.Get("Product"))
   260  				respCheck := func(resp *http.Response) {
   261  					if self.cpcfg.UpdatePermission != nil {
   262  						body, err := ioutil.ReadAll(resp.Body)
   263  						if err != nil {
   264  							return
   265  						}
   266  						resp.Body = ioutil.NopCloser(bytes.NewBuffer(body))
   267  						obj, err := jsonutils.Parse(body)
   268  						if err != nil {
   269  							return
   270  						}
   271  						ret := struct {
   272  							AsapiErrorCode string `json:"asapiErrorCode"`
   273  							Code           string
   274  						}{}
   275  						obj.Unmarshal(&ret)
   276  						if ret.Code == "403" ||
   277  							strings.Contains(ret.AsapiErrorCode, "NoPermission") ||
   278  							utils.HasPrefix(ret.Code, "Forbidden") ||
   279  							utils.HasPrefix(ret.Code, "NoPermission") {
   280  							self.cpcfg.UpdatePermission(service, action)
   281  						}
   282  					}
   283  				}
   284  				if self.cpcfg.ReadOnly {
   285  					for _, prefix := range []string{"Get", "List", "Describe"} {
   286  						if strings.HasPrefix(action, prefix) {
   287  							return respCheck, nil
   288  						}
   289  					}
   290  					return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, action)
   291  				}
   292  				return respCheck, nil
   293  			}),
   294  		},
   295  		&credentials.BaseCredential{
   296  			AccessKeyId:     self.accessKey,
   297  			AccessKeySecret: self.accessSecret,
   298  		},
   299  	)
   300  	return client, err
   301  }
   302  
   303  func (self *SApsaraClient) ascmRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
   304  	cli, err := self.getDefaultClient("")
   305  	if err != nil {
   306  		return nil, err
   307  	}
   308  	return productRequest(cli, APSARA_PRODUCT_ASCM, self.cpcfg.URL, APSARA_ASCM_API_VERSION, apiName, params, self.debug)
   309  }
   310  
   311  func (self *SApsaraClient) ecsRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
   312  	cli, err := self.getDefaultClient("")
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	domain := self.getDomain(APSARA_PRODUCT_ECS)
   317  	return productRequest(cli, APSARA_PRODUCT_ECS, domain, APSARA_API_VERSION, apiName, params, self.debug)
   318  }
   319  
   320  func (self *SApsaraClient) ossRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
   321  	cli, err := self.getDefaultClient("")
   322  	if err != nil {
   323  		return nil, err
   324  	}
   325  	//pm := map[string]string{}
   326  	//for k, v := range params {
   327  	//	if k != "RegionId" {
   328  	//		pm[k] = v
   329  	//		delete(params, k)
   330  	//	}
   331  	//}
   332  	//if len(pm) > 0 {
   333  	//	params["Params"] = jsonutils.Marshal(pm).String()
   334  	//}
   335  	if _, ok := params["RegionId"]; !ok {
   336  		params["RegionId"] = self.cpcfg.DefaultRegion
   337  	}
   338  	params["ProductName"] = "oss"
   339  	params["OpenApiAction"] = apiName
   340  	return productRequest(cli, "OneRouter", self.cpcfg.URL, "2018-12-12", "DoOpenApi", params, self.debug)
   341  }
   342  
   343  func (self *SApsaraClient) trialRequest(apiName string, params map[string]string) (jsonutils.JSONObject, error) {
   344  	cli, err := self.getDefaultClient("")
   345  	if err != nil {
   346  		return nil, err
   347  	}
   348  	domain := self.getDomain(APSARA_PRODUCT_ACTION_TRIAL)
   349  	return productRequest(cli, APSARA_PRODUCT_ACTION_TRIAL, domain, APSARA_API_VERSION_TRIAL, apiName, params, self.debug)
   350  }
   351  
   352  func (self *SApsaraClient) fetchRegions() error {
   353  	params := map[string]string{"AcceptLanguage": "zh-CN"}
   354  	if len(self.cpcfg.DefaultRegion) > 0 {
   355  		params["RegionId"] = self.cpcfg.DefaultRegion
   356  	}
   357  	body, err := self.ecsRequest("DescribeRegions", params)
   358  	if err != nil {
   359  		return errors.Wrapf(err, "DescribeRegions")
   360  	}
   361  
   362  	regions := make([]SRegion, 0)
   363  	err = body.Unmarshal(&regions, "Regions", "Region")
   364  	if err != nil {
   365  		return errors.Wrapf(err, "body.Unmarshal")
   366  	}
   367  	self.iregions = make([]cloudprovider.ICloudRegion, len(regions))
   368  	for i := 0; i < len(regions); i += 1 {
   369  		regions[i].client = self
   370  		self.iregions[i] = &regions[i]
   371  	}
   372  	return nil
   373  }
   374  
   375  // https://help.apsara.com/document_detail/31837.html?spm=a2c4g.11186623.2.6.XqEgD1
   376  func (client *SApsaraClient) getOssClient(endpoint string) (*oss.Client, error) {
   377  	// NOTE
   378  	//
   379  	// oss package as of version 20181116160301-c6838fdc33ed does not
   380  	// respect http.ProxyFromEnvironment.
   381  	//
   382  	// The ClientOption Proxy, AuthProxy lacks the feature NO_PROXY has
   383  	// which can be used to whitelist ips, domains from http_proxy,
   384  	// https_proxy setting
   385  	// oss use no timeout client so as to send/download large files
   386  	httpClient := client.cpcfg.AdaptiveTimeoutHttpClient()
   387  	transport, _ := httpClient.Transport.(*http.Transport)
   388  	httpClient.Transport = cloudprovider.GetCheckTransport(transport, func(req *http.Request) (func(resp *http.Response), error) {
   389  		if client.cpcfg.ReadOnly {
   390  			if req.Method == "GET" || req.Method == "HEAD" {
   391  				return nil, nil
   392  			}
   393  			return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
   394  		}
   395  		return nil, nil
   396  	})
   397  	cliOpts := []oss.ClientOption{
   398  		oss.HTTPClient(httpClient),
   399  	}
   400  	cli, err := oss.New(endpoint, client.accessKey, client.accessSecret, cliOpts...)
   401  	if err != nil {
   402  		return nil, errors.Wrap(err, "oss.New")
   403  	}
   404  	return cli, nil
   405  }
   406  
   407  func (self *SApsaraClient) GetRegions() []SRegion {
   408  	regions := make([]SRegion, len(self.iregions))
   409  	for i := 0; i < len(regions); i += 1 {
   410  		region := self.iregions[i].(*SRegion)
   411  		regions[i] = *region
   412  	}
   413  	return regions
   414  }
   415  
   416  func (self *SApsaraClient) GetProvider() string {
   417  	return self.cpcfg.Vendor
   418  }
   419  
   420  func (self *SApsaraClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
   421  	err := self.fetchRegions()
   422  	if err != nil {
   423  		return nil, err
   424  	}
   425  	subAccount := cloudprovider.SSubAccount{}
   426  	subAccount.Name = self.cpcfg.Name
   427  	subAccount.Account = self.accessKey
   428  	if self.organizationId != "1" {
   429  		subAccount.Account = fmt.Sprintf("%s/%s", self.accessKey, self.organizationId)
   430  	}
   431  	subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
   432  	return []cloudprovider.SSubAccount{subAccount}, nil
   433  }
   434  
   435  func (self *SApsaraClient) GetAccountId() string {
   436  	return self.cpcfg.URL
   437  }
   438  
   439  func (self *SApsaraClient) GetIRegions() []cloudprovider.ICloudRegion {
   440  	return self.iregions
   441  }
   442  
   443  func (self *SApsaraClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
   444  	for i := 0; i < len(self.iregions); i += 1 {
   445  		if self.iregions[i].GetGlobalId() == id {
   446  			return self.iregions[i], nil
   447  		}
   448  	}
   449  	return nil, cloudprovider.ErrNotFound
   450  }
   451  
   452  func (self *SApsaraClient) GetRegion(regionId string) *SRegion {
   453  	for i := 0; i < len(self.iregions); i += 1 {
   454  		if self.iregions[i].GetId() == regionId {
   455  			return self.iregions[i].(*SRegion)
   456  		}
   457  	}
   458  	return nil
   459  }
   460  
   461  func (self *SApsaraClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) {
   462  	for i := 0; i < len(self.iregions); i += 1 {
   463  		ihost, err := self.iregions[i].GetIHostById(id)
   464  		if err == nil {
   465  			return ihost, nil
   466  		} else if err != cloudprovider.ErrNotFound {
   467  			return nil, err
   468  		}
   469  	}
   470  	return nil, cloudprovider.ErrNotFound
   471  }
   472  
   473  func (self *SApsaraClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) {
   474  	for i := 0; i < len(self.iregions); i += 1 {
   475  		ihost, err := self.iregions[i].GetIVpcById(id)
   476  		if err == nil {
   477  			return ihost, nil
   478  		} else if err != cloudprovider.ErrNotFound {
   479  			return nil, err
   480  		}
   481  	}
   482  	return nil, cloudprovider.ErrNotFound
   483  }
   484  
   485  func (self *SApsaraClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) {
   486  	for i := 0; i < len(self.iregions); i += 1 {
   487  		ihost, err := self.iregions[i].GetIStorageById(id)
   488  		if err == nil {
   489  			return ihost, nil
   490  		} else if err != cloudprovider.ErrNotFound {
   491  			return nil, err
   492  		}
   493  	}
   494  	return nil, cloudprovider.ErrNotFound
   495  }
   496  
   497  func (region *SApsaraClient) GetCapabilities() []string {
   498  	caps := []string{
   499  		cloudprovider.CLOUD_CAPABILITY_PROJECT,
   500  		cloudprovider.CLOUD_CAPABILITY_COMPUTE,
   501  		cloudprovider.CLOUD_CAPABILITY_NETWORK,
   502  		cloudprovider.CLOUD_CAPABILITY_EIP,
   503  		cloudprovider.CLOUD_CAPABILITY_LOADBALANCER,
   504  		cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE,
   505  		cloudprovider.CLOUD_CAPABILITY_RDS,
   506  		cloudprovider.CLOUD_CAPABILITY_CACHE,
   507  		cloudprovider.CLOUD_CAPABILITY_QUOTA + cloudprovider.READ_ONLY_SUFFIX,
   508  		cloudprovider.CLOUD_CAPABILITY_IPV6_GATEWAY + cloudprovider.READ_ONLY_SUFFIX,
   509  		cloudprovider.CLOUD_CAPABILITY_TABLESTORE + cloudprovider.READ_ONLY_SUFFIX,
   510  	}
   511  	return caps
   512  }