yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/ucloud/ucloud.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 ucloud
    16  
    17  import (
    18  	"bytes"
    19  	"fmt"
    20  	"io/ioutil"
    21  	"net/http"
    22  	"strings"
    23  
    24  	"yunion.io/x/jsonutils"
    25  	"yunion.io/x/log"
    26  	"yunion.io/x/pkg/errors"
    27  
    28  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    29  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    30  )
    31  
    32  /*
    33  UCLOUD 项目:https://docs.ucloud.cn/management_monitor/uproject/projects
    34  项目可认为是云账户下承载资源的容器,当您注册一个UCloud云账户后,系统会默认创建一个项目,您属于的资源都落在此项目下。如您有新的业务要使用云服务,可创建一个新项目,并将新业务部署在新项目下,实现业务之间的网络与逻辑隔离。
    35  
    36  1、项目之间默认网络与逻辑隔离,即项目A的主机无法绑定项目B的EIP,默认也无法与项目B的主机内网通信。但联通项目后,uhost、udb、umem可实现内网通信。
    37  
    38  2、资源不能在项目间迁移,即项目A内的主机无法迁移至项目B,因其不在一个基础网络内,且逻辑上也是隔离的。但诸如自主镜像等静态资源,您可以提交工单申请迁移至其他项目。
    39  
    40  3、只有云账户本身,才能删除项目,且必须是项目被没有资源、没有任何子成员、未与其他项目联通的情况下才可删除。
    41  
    42  
    43  UCloud DiskType貌似也是一个奇葩的存在
    44  // https://docs.ucloud.cn/api/uhost-api/disk_type
    45  1.在主机创建查询接口中 DISK type 对应 CLOUD_SSD|CLOUD_NORMAL|...
    46  2.在数据盘创建中对应  DataDisk|SSDDataDisk
    47  3.在数据盘查询接口请求中对应   DataDisk|SystemDisk 。在结果中对应DataDisk|SSDDataDisk|SSDSystemDisk|SystemDisk
    48  
    49  目前存在的问题:
    50  1.很多国外区域都需要单独申请开通权限才能使用。onecloud有可能调度到未开通权限区域导致失败。
    51  */
    52  
    53  const (
    54  	CLOUD_PROVIDER_UCLOUD    = api.CLOUD_PROVIDER_UCLOUD
    55  	CLOUD_PROVIDER_UCLOUD_CN = "UCloud"
    56  
    57  	UCLOUD_DEFAULT_REGION = "cn-bj2"
    58  
    59  	UCLOUD_API_VERSION = "2019-02-28"
    60  )
    61  
    62  type UcloudClientConfig struct {
    63  	cpcfg cloudprovider.ProviderConfig
    64  
    65  	accessKeyId     string
    66  	accessKeySecret string
    67  	projectId       string
    68  
    69  	debug bool
    70  }
    71  
    72  func NewUcloudClientConfig(accessKeyId, accessKeySecret string) *UcloudClientConfig {
    73  	cfg := &UcloudClientConfig{
    74  		accessKeyId:     accessKeyId,
    75  		accessKeySecret: accessKeySecret,
    76  	}
    77  	return cfg
    78  }
    79  
    80  func (cfg *UcloudClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *UcloudClientConfig {
    81  	cfg.cpcfg = cpcfg
    82  	return cfg
    83  }
    84  
    85  func (cfg *UcloudClientConfig) ProjectId(projectId string) *UcloudClientConfig {
    86  	cfg.projectId = projectId
    87  	return cfg
    88  }
    89  
    90  func (cfg *UcloudClientConfig) Debug(debug bool) *UcloudClientConfig {
    91  	cfg.debug = debug
    92  	return cfg
    93  }
    94  
    95  type SUcloudClient struct {
    96  	*UcloudClientConfig
    97  
    98  	iregions []cloudprovider.ICloudRegion
    99  	iBuckets []cloudprovider.ICloudBucket
   100  
   101  	httpClient *http.Client
   102  }
   103  
   104  // 进行资源操作时参数account 对应数据库cloudprovider表中的account字段,由accessKey和projectID两部分组成,通过"/"分割。
   105  // 初次导入Subaccount时,参数account对应cloudaccounts表中的account字段,即accesskey。此时projectID为空,只能进行同步子账号(项目)、查询region列表等projectId无关的操作。
   106  func NewUcloudClient(cfg *UcloudClientConfig) (*SUcloudClient, error) {
   107  	httpClient := cfg.cpcfg.AdaptiveTimeoutHttpClient()
   108  	ts, _ := httpClient.Transport.(*http.Transport)
   109  	httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) {
   110  		if cfg.cpcfg.ReadOnly {
   111  			if req.ContentLength > 0 {
   112  				body, err := ioutil.ReadAll(req.Body)
   113  				if err != nil {
   114  					return nil, errors.Wrapf(err, "ioutil.ReadAll")
   115  				}
   116  				req.Body = ioutil.NopCloser(bytes.NewBuffer(body))
   117  				obj, err := jsonutils.Parse(body)
   118  				if err != nil {
   119  					return nil, errors.Wrapf(err, "Parse request body")
   120  				}
   121  				action, err := obj.GetString("Action")
   122  				if err != nil {
   123  					return nil, errors.Wrapf(err, "Get request action")
   124  				}
   125  				for _, prefix := range []string{"Get", "Describe", "List"} {
   126  					if strings.HasPrefix(action, prefix) {
   127  						return nil, nil
   128  					}
   129  				}
   130  				return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
   131  			}
   132  		}
   133  		return nil, nil
   134  	})
   135  	client := SUcloudClient{
   136  		UcloudClientConfig: cfg,
   137  		httpClient:         httpClient,
   138  	}
   139  
   140  	err := client.fetchRegions()
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	err = client.fetchBuckets()
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	return &client, nil
   150  }
   151  
   152  func (self *SUcloudClient) UpdateAccount(accessKey, secret string) error {
   153  	if self.accessKeyId != accessKey || self.accessKeySecret != secret {
   154  		self.accessKeyId = accessKey
   155  		self.accessKeySecret = secret
   156  		return self.fetchRegions()
   157  	} else {
   158  		return nil
   159  	}
   160  }
   161  
   162  func (self *SUcloudClient) commonParams(params SParams, action string) (string, SParams) {
   163  	resultKey, exists := UCLOUD_API_RESULT_KEYS[action]
   164  	if !exists || len(resultKey) == 0 {
   165  		// default key for describe actions
   166  		if strings.HasPrefix(action, "Describe") {
   167  			resultKey = "DataSet"
   168  		}
   169  	}
   170  
   171  	if len(self.projectId) > 0 {
   172  		params.Set("ProjectId", self.projectId)
   173  	}
   174  	params.Set("PublicKey", self.accessKeyId)
   175  
   176  	return resultKey, params
   177  }
   178  
   179  func (self *SUcloudClient) DoListAll(action string, params SParams, result interface{}) error {
   180  	resultKey, params := self.commonParams(params, action)
   181  	return DoListAll(self, action, params, resultKey, result)
   182  }
   183  
   184  func (self *SUcloudClient) DoListPart(action string, limit int, offset int, params SParams, result interface{}) (int, int, error) {
   185  	resultKey, params := self.commonParams(params, action)
   186  	params.SetPagination(limit, offset)
   187  	return doListPart(self, action, params, resultKey, result)
   188  }
   189  
   190  func (self *SUcloudClient) DoAction(action string, params SParams, result interface{}) error {
   191  	resultKey, params := self.commonParams(params, action)
   192  	err := DoAction(self, action, params, resultKey, result)
   193  	if err != nil {
   194  		return err
   195  	}
   196  
   197  	return nil
   198  }
   199  
   200  func (self *SUcloudClient) fetchRegions() error {
   201  	type Region struct {
   202  		RegionID   int64  `json:"RegionId"`
   203  		RegionName string `json:"RegionName"`
   204  		IsDefault  bool   `json:"IsDefault"`
   205  		BitMaps    string `json:"BitMaps"`
   206  		Region     string `json:"Region"`
   207  		Zone       string `json:"Zone"`
   208  	}
   209  
   210  	params := NewUcloudParams()
   211  	regions := make([]Region, 0)
   212  	err := self.DoListAll("GetRegion", params, &regions)
   213  	if err != nil {
   214  		return err
   215  	}
   216  
   217  	regionSet := make(map[string]string, 0)
   218  	for _, region := range regions {
   219  		regionSet[region.Region] = region.Region
   220  	}
   221  
   222  	sregions := make([]SRegion, len(regionSet))
   223  	self.iregions = make([]cloudprovider.ICloudRegion, len(regionSet))
   224  	i := 0
   225  	for regionId := range regionSet {
   226  		sregions[i].client = self
   227  		sregions[i].RegionID = regionId
   228  		self.iregions[i] = &sregions[i]
   229  		i += 1
   230  	}
   231  
   232  	return nil
   233  }
   234  
   235  func (client *SUcloudClient) invalidateIBuckets() {
   236  	client.iBuckets = nil
   237  }
   238  
   239  func (client *SUcloudClient) getIBuckets() ([]cloudprovider.ICloudBucket, error) {
   240  	if client.iBuckets == nil {
   241  		err := client.fetchBuckets()
   242  		if err != nil {
   243  			return nil, errors.Wrap(err, "fetchBuckets")
   244  		}
   245  	}
   246  	return client.iBuckets, nil
   247  }
   248  
   249  func (client *SUcloudClient) fetchBuckets() error {
   250  	buckets := make([]SBucket, 0)
   251  	offset := 0
   252  	limit := 50
   253  	for {
   254  		parts, err := client.listBuckets("", offset, limit)
   255  		if err != nil {
   256  			return errors.Wrap(err, "client.listBuckets")
   257  		}
   258  		if len(parts) > 0 {
   259  			buckets = append(buckets, parts...)
   260  		}
   261  		if len(parts) < limit {
   262  			break
   263  		} else {
   264  			offset += limit
   265  		}
   266  	}
   267  	ret := make([]cloudprovider.ICloudBucket, 0)
   268  	for i := range buckets {
   269  		region, err := client.getIRegionByRegionId(buckets[i].Region)
   270  		if err != nil {
   271  			log.Errorf("fail to find iregion %s", buckets[i].Region)
   272  			continue
   273  		}
   274  		buckets[i].region = region.(*SRegion)
   275  		ret = append(ret, &buckets[i])
   276  	}
   277  
   278  	client.iBuckets = ret
   279  
   280  	return nil
   281  }
   282  
   283  func (self *SUcloudClient) GetRegions() []SRegion {
   284  	regions := make([]SRegion, len(self.iregions))
   285  	for i := 0; i < len(regions); i += 1 {
   286  		region := self.iregions[i].(*SRegion)
   287  		regions[i] = *region
   288  	}
   289  	return regions
   290  }
   291  
   292  func (self *SUcloudClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
   293  	projects, err := self.FetchProjects()
   294  	if err != nil {
   295  		return nil, err
   296  	}
   297  
   298  	subAccounts := make([]cloudprovider.SSubAccount, 0)
   299  	for _, project := range projects {
   300  		subAccount := cloudprovider.SSubAccount{}
   301  		subAccount.Name = fmt.Sprintf("%s-%s", self.cpcfg.Name, project.ProjectName)
   302  		// ucloud账号ID中可能包含/。因此使用::作为分割符号
   303  		subAccount.Account = fmt.Sprintf("%s::%s", self.accessKeyId, project.ProjectID)
   304  		subAccount.HealthStatus = api.CLOUD_PROVIDER_HEALTH_NORMAL
   305  
   306  		subAccounts = append(subAccounts, subAccount)
   307  	}
   308  
   309  	return subAccounts, nil
   310  }
   311  
   312  func (self *SUcloudClient) GetAccountId() string {
   313  	return "" // no account ID found for ucloud
   314  }
   315  
   316  func (self *SUcloudClient) GetIRegions() []cloudprovider.ICloudRegion {
   317  	return self.iregions
   318  }
   319  
   320  func removeDigit(idstr string) string {
   321  	for len(idstr) > 0 && idstr[len(idstr)-1] >= '0' && idstr[len(idstr)-1] <= '9' {
   322  		idstr = idstr[:len(idstr)-1]
   323  	}
   324  	return idstr
   325  }
   326  
   327  func (self *SUcloudClient) getIRegionByRegionId(id string) (cloudprovider.ICloudRegion, error) {
   328  	for i := 0; i < len(self.iregions); i += 1 {
   329  		if self.iregions[i].GetId() == id {
   330  			return self.iregions[i], nil
   331  		}
   332  	}
   333  	// retry
   334  	for i := 0; i < len(self.iregions); i += 1 {
   335  		rid := removeDigit(self.iregions[i].GetId())
   336  		rid2 := removeDigit(id)
   337  		if rid == rid2 {
   338  			return self.iregions[i], nil
   339  		}
   340  	}
   341  	return nil, cloudprovider.ErrNotFound
   342  }
   343  
   344  func (self *SUcloudClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
   345  	for i := 0; i < len(self.iregions); i += 1 {
   346  		if self.iregions[i].GetGlobalId() == id {
   347  			return self.iregions[i], nil
   348  		}
   349  	}
   350  	return nil, cloudprovider.ErrNotFound
   351  }
   352  
   353  func (self *SUcloudClient) GetRegion(regionId string) *SRegion {
   354  	if len(regionId) == 0 {
   355  		regionId = UCLOUD_DEFAULT_REGION
   356  	}
   357  	for i := 0; i < len(self.iregions); i += 1 {
   358  		if self.iregions[i].GetId() == regionId {
   359  			return self.iregions[i].(*SRegion)
   360  		}
   361  	}
   362  	return nil
   363  }
   364  
   365  func (self *SUcloudClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) {
   366  	for i := 0; i < len(self.iregions); i += 1 {
   367  		ihost, err := self.iregions[i].GetIHostById(id)
   368  		if err == nil {
   369  			return ihost, nil
   370  		} else if errors.Cause(err) != cloudprovider.ErrNotFound {
   371  			return nil, err
   372  		}
   373  	}
   374  	return nil, cloudprovider.ErrNotFound
   375  }
   376  
   377  func (self *SUcloudClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) {
   378  	for i := 0; i < len(self.iregions); i += 1 {
   379  		ihost, err := self.iregions[i].GetIVpcById(id)
   380  		if err == nil {
   381  			return ihost, nil
   382  		} else if errors.Cause(err) != cloudprovider.ErrNotFound {
   383  			return nil, err
   384  		}
   385  	}
   386  	return nil, cloudprovider.ErrNotFound
   387  }
   388  
   389  func (self *SUcloudClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) {
   390  	for i := 0; i < len(self.iregions); i += 1 {
   391  		ihost, err := self.iregions[i].GetIStorageById(id)
   392  		if err == nil {
   393  			return ihost, nil
   394  		} else if errors.Cause(err) != cloudprovider.ErrNotFound {
   395  			return nil, err
   396  		}
   397  	}
   398  	return nil, cloudprovider.ErrNotFound
   399  }
   400  
   401  func (self *SUcloudClient) GetCapabilities() []string {
   402  	caps := []string{
   403  		// cloudprovider.CLOUD_CAPABILITY_PROJECT,
   404  		cloudprovider.CLOUD_CAPABILITY_COMPUTE,
   405  		cloudprovider.CLOUD_CAPABILITY_NETWORK,
   406  		cloudprovider.CLOUD_CAPABILITY_EIP,
   407  		// cloudprovider.CLOUD_CAPABILITY_LOADBALANCER,
   408  		// cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE,
   409  		// cloudprovider.CLOUD_CAPABILITY_RDS,
   410  		// cloudprovider.CLOUD_CAPABILITY_CACHE,
   411  		// cloudprovider.CLOUD_CAPABILITY_EVENT,
   412  	}
   413  	return caps
   414  }