yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/hcso/huawei.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 hcso
    16  
    17  import (
    18  	"fmt"
    19  	"net/http"
    20  	"strings"
    21  	"time"
    22  
    23  	"yunion.io/x/log"
    24  	"yunion.io/x/pkg/errors"
    25  	"yunion.io/x/pkg/util/timeutils"
    26  
    27  	api "yunion.io/x/cloudmux/pkg/apis/compute"
    28  	"yunion.io/x/cloudmux/pkg/cloudprovider"
    29  	"yunion.io/x/cloudmux/pkg/multicloud/hcso/client"
    30  	"yunion.io/x/cloudmux/pkg/multicloud/hcso/client/auth"
    31  	"yunion.io/x/cloudmux/pkg/multicloud/hcso/client/auth/credentials"
    32  	"yunion.io/x/cloudmux/pkg/multicloud/huawei/obs"
    33  )
    34  
    35  /*
    36  待解决问题:
    37  2.VM密码登录不成功(ubuntu不行,centos可以)
    38  3.实例绑定eip 查不出来eip?
    39  */
    40  
    41  const (
    42  	CLOUD_PROVIDER_HUAWEI    = api.CLOUD_PROVIDER_HCSO
    43  	CLOUD_PROVIDER_HUAWEI_CN = "华为云Stack"
    44  	CLOUD_PROVIDER_HUAWEI_EN = "HCSO"
    45  
    46  	HUAWEI_API_VERSION = ""
    47  )
    48  
    49  var HUAWEI_REGION_CACHES = map[string]userRegionsCache{}
    50  
    51  type userRegionsCache struct {
    52  	UserId   string
    53  	ExpireAt time.Time
    54  	Regions  []SRegion
    55  }
    56  
    57  type HuaweiClientConfig struct {
    58  	cpcfg     cloudprovider.ProviderConfig
    59  	endpoints *cloudprovider.SHCSOEndpoints
    60  
    61  	projectId    string // 华为云项目ID.
    62  	accessKey    string
    63  	accessSecret string
    64  
    65  	debug bool
    66  }
    67  
    68  func NewHuaweiClientConfig(accessKey, accessSecret, projectId string, endpoints *cloudprovider.SHCSOEndpoints) *HuaweiClientConfig {
    69  	cfg := &HuaweiClientConfig{
    70  		projectId:    projectId,
    71  		accessKey:    accessKey,
    72  		accessSecret: accessSecret,
    73  		endpoints:    endpoints,
    74  	}
    75  
    76  	return cfg
    77  }
    78  
    79  func (cfg *HuaweiClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *HuaweiClientConfig {
    80  	cfg.cpcfg = cpcfg
    81  	return cfg
    82  }
    83  
    84  func (cfg *HuaweiClientConfig) Debug(debug bool) *HuaweiClientConfig {
    85  	cfg.debug = debug
    86  	return cfg
    87  }
    88  
    89  type SHuaweiClient struct {
    90  	*HuaweiClientConfig
    91  
    92  	signer auth.Signer
    93  
    94  	isMainProject bool // whether the project is the main project in the region
    95  
    96  	ownerId         string
    97  	ownerName       string
    98  	ownerCreateTime time.Time
    99  
   100  	iregions []cloudprovider.ICloudRegion
   101  	iBuckets []cloudprovider.ICloudBucket
   102  
   103  	projects []SProject
   104  	regions  []SRegion
   105  
   106  	httpClient *http.Client
   107  }
   108  
   109  // 进行资源操作时参数account 对应数据库cloudprovider表中的account字段,由accessKey和projectID两部分组成,通过"/"分割。
   110  // 初次导入Subaccount时,参数account对应cloudaccounts表中的account字段,即accesskey。此时projectID为空,
   111  // 只能进行同步子账号、查询region列表等projectId无关的操作。
   112  func NewHuaweiClient(cfg *HuaweiClientConfig) (*SHuaweiClient, error) {
   113  	client := SHuaweiClient{
   114  		HuaweiClientConfig: cfg,
   115  	}
   116  
   117  	err := client.init()
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  	return &client, nil
   122  }
   123  
   124  func (self *SHuaweiClient) init() error {
   125  	err := self.fetchRegions()
   126  	if err != nil {
   127  		return err
   128  	}
   129  	err = self.initSigner()
   130  	if err != nil {
   131  		return errors.Wrap(err, "initSigner")
   132  	}
   133  	err = self.initOwner()
   134  	if err != nil {
   135  		return errors.Wrap(err, "fetchOwner")
   136  	}
   137  	if self.debug {
   138  		log.Debugf("OwnerId: %s name: %s", self.ownerId, self.ownerName)
   139  	}
   140  	return nil
   141  }
   142  
   143  func (self *SHuaweiClient) initSigner() error {
   144  	var err error
   145  	cred := credentials.NewAccessKeyCredential(self.accessKey, self.accessKey)
   146  	self.signer, err = auth.NewSignerWithCredential(cred)
   147  	if err != nil {
   148  		return err
   149  	}
   150  	return nil
   151  }
   152  
   153  func (self *SHuaweiClient) newRegionAPIClient(regionId string) (*client.Client, error) {
   154  	cli, err := client.NewClientWithAccessKey(regionId, self.ownerId, self.projectId, self.accessKey, self.accessSecret, self.debug, self.cpcfg.DefaultRegion, self.endpoints)
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	httpClient := self.cpcfg.AdaptiveTimeoutHttpClient()
   160  	ts, _ := httpClient.Transport.(*http.Transport)
   161  	httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) {
   162  		if self.cpcfg.ReadOnly {
   163  			if req.Method == "GET" {
   164  				return nil, nil
   165  			}
   166  			return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
   167  		}
   168  		return nil, nil
   169  	})
   170  	cli.SetHttpClient(httpClient)
   171  
   172  	return cli, nil
   173  }
   174  
   175  func (self *SHuaweiClient) newGeneralAPIClient() (*client.Client, error) {
   176  	cli, err := client.NewClientWithAccessKey(self.cpcfg.DefaultRegion, self.ownerId, "", self.accessKey, self.accessSecret, self.debug, self.cpcfg.DefaultRegion, self.endpoints)
   177  	if err != nil {
   178  		return nil, err
   179  	}
   180  
   181  	httpClient := self.cpcfg.AdaptiveTimeoutHttpClient()
   182  	ts, _ := httpClient.Transport.(*http.Transport)
   183  	httpClient.Transport = cloudprovider.GetCheckTransport(ts, func(req *http.Request) (func(resp *http.Response), error) {
   184  		if self.cpcfg.ReadOnly {
   185  			if req.Method == "GET" {
   186  				return nil, nil
   187  			}
   188  			return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path)
   189  		}
   190  		return nil, nil
   191  	})
   192  	cli.SetHttpClient(httpClient)
   193  
   194  	return cli, nil
   195  }
   196  
   197  func (self *SHuaweiClient) fetchRegions() error {
   198  	huawei, _ := self.newGeneralAPIClient()
   199  	if self.regions == nil {
   200  		userId, err := self.GetUserId()
   201  		if err != nil {
   202  			return errors.Wrap(err, "GetUserId")
   203  		}
   204  
   205  		if regionsCache, ok := HUAWEI_REGION_CACHES[userId]; !ok || regionsCache.ExpireAt.Sub(time.Now()).Seconds() > 0 {
   206  			regions := make([]SRegion, 0)
   207  			err := doListAll(huawei.Regions.List, nil, &regions)
   208  			if err != nil {
   209  				return errors.Wrap(err, "Regions.List")
   210  			}
   211  
   212  			HUAWEI_REGION_CACHES[userId] = userRegionsCache{ExpireAt: time.Now().Add(24 * time.Hour), UserId: userId, Regions: regions}
   213  		}
   214  
   215  		self.regions = HUAWEI_REGION_CACHES[userId].Regions
   216  	}
   217  
   218  	filtedRegions := make([]SRegion, 0)
   219  	if len(self.projectId) > 0 {
   220  		project, err := self.GetProjectById(self.projectId)
   221  		if err != nil {
   222  			return err
   223  		}
   224  
   225  		regionId := strings.Split(project.Name, "_")[0]
   226  		for _, region := range self.regions {
   227  			if region.ID == regionId {
   228  				filtedRegions = append(filtedRegions, region)
   229  			}
   230  		}
   231  		if regionId == project.Name {
   232  			self.isMainProject = true
   233  		}
   234  	} else {
   235  		filtedRegions = self.regions
   236  	}
   237  
   238  	self.iregions = make([]cloudprovider.ICloudRegion, len(filtedRegions))
   239  	for i := 0; i < len(filtedRegions); i += 1 {
   240  		filtedRegions[i].client = self
   241  		_, err := filtedRegions[i].getECSClient()
   242  		if err != nil {
   243  			return err
   244  		}
   245  		self.iregions[i] = &filtedRegions[i]
   246  	}
   247  	return nil
   248  }
   249  
   250  func (self *SHuaweiClient) invalidateIBuckets() {
   251  	self.iBuckets = nil
   252  }
   253  
   254  func (self *SHuaweiClient) getIBuckets() ([]cloudprovider.ICloudBucket, error) {
   255  	if self.iBuckets == nil {
   256  		err := self.fetchBuckets()
   257  		if err != nil {
   258  			return nil, errors.Wrap(err, "fetchBuckets")
   259  		}
   260  	}
   261  	return self.iBuckets, nil
   262  }
   263  
   264  func getOBSEndpoint(regionId string) string {
   265  	return fmt.Sprintf("obs.%s.myhuaweicloud.com", regionId)
   266  }
   267  
   268  func (client *SHuaweiClient) getOBSClient(regionId string) (*obs.ObsClient, error) {
   269  	endpoint := client.endpoints.GetEndpoint(client.cpcfg.DefaultRegion, "obs", regionId)
   270  	return obs.New(client.accessKey, client.accessSecret, endpoint)
   271  }
   272  
   273  func (self *SHuaweiClient) fetchBuckets() error {
   274  	obscli, err := self.getOBSClient(self.cpcfg.DefaultRegion)
   275  	if err != nil {
   276  		return errors.Wrap(err, "getOBSClient")
   277  	}
   278  	input := &obs.ListBucketsInput{QueryLocation: true}
   279  	output, err := obscli.ListBuckets(input)
   280  	if err != nil {
   281  		return errors.Wrap(err, "obscli.ListBuckets")
   282  	}
   283  	self.ownerId = output.Owner.ID
   284  
   285  	ret := make([]cloudprovider.ICloudBucket, 0)
   286  	for i := range output.Buckets {
   287  		bInfo := output.Buckets[i]
   288  		region, err := self.getIRegionByRegionId(bInfo.Location)
   289  		if err != nil {
   290  			log.Errorf("fail to find region %s", bInfo.Location)
   291  			continue
   292  		}
   293  		b := SBucket{
   294  			region: region.(*SRegion),
   295  
   296  			Name:         bInfo.Name,
   297  			Location:     bInfo.Location,
   298  			CreationDate: bInfo.CreationDate,
   299  		}
   300  		ret = append(ret, &b)
   301  	}
   302  	self.iBuckets = ret
   303  	return nil
   304  }
   305  
   306  func (self *SHuaweiClient) GetCloudRegionExternalIdPrefix() string {
   307  	if len(self.projectId) > 0 {
   308  		return self.iregions[0].GetGlobalId()
   309  	} else {
   310  		return CLOUD_PROVIDER_HUAWEI
   311  	}
   312  }
   313  
   314  func (self *SHuaweiClient) UpdateAccount(accessKey, secret string) error {
   315  	if self.accessKey != accessKey || self.accessSecret != secret {
   316  		self.accessKey = accessKey
   317  		self.accessSecret = secret
   318  		return self.fetchRegions()
   319  	} else {
   320  		return nil
   321  	}
   322  }
   323  
   324  func (self *SHuaweiClient) GetRegions() []SRegion {
   325  	regions := make([]SRegion, len(self.iregions))
   326  	for i := 0; i < len(regions); i += 1 {
   327  		region := self.iregions[i].(*SRegion)
   328  		regions[i] = *region
   329  	}
   330  	return regions
   331  }
   332  
   333  func (self *SHuaweiClient) GetSubAccounts() ([]cloudprovider.SSubAccount, error) {
   334  	projects, err := self.fetchProjects()
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	// https://support.huaweicloud.com/api-iam/zh-cn_topic_0074171149.html
   340  	subAccounts := make([]cloudprovider.SSubAccount, 0)
   341  	for i := range projects {
   342  		project := projects[i]
   343  		// name 为MOS的project是华为云内部的一个特殊project。不需要同步到本地
   344  		if strings.ToLower(project.Name) == "mos" {
   345  			continue
   346  		}
   347  
   348  		s := cloudprovider.SSubAccount{
   349  			Name:         fmt.Sprintf("%s-%s", self.cpcfg.Name, project.Name),
   350  			Account:      fmt.Sprintf("%s/%s", self.accessKey, project.ID),
   351  			HealthStatus: project.GetHealthStatus(),
   352  		}
   353  
   354  		subAccounts = append(subAccounts, s)
   355  	}
   356  
   357  	return subAccounts, nil
   358  }
   359  
   360  func (client *SHuaweiClient) GetAccountId() string {
   361  	return client.ownerId
   362  }
   363  
   364  func (client *SHuaweiClient) GetIamLoginUrl() string {
   365  	return fmt.Sprintf("https://auth.huaweicloud.com/authui/login.html?account=%s#/login", client.ownerName)
   366  }
   367  
   368  func (self *SHuaweiClient) GetIRegions() []cloudprovider.ICloudRegion {
   369  	return self.iregions
   370  }
   371  
   372  func (self *SHuaweiClient) getIRegionByRegionId(id string) (cloudprovider.ICloudRegion, error) {
   373  	for i := 0; i < len(self.iregions); i += 1 {
   374  		log.Debugf("%d ID: %s", i, self.iregions[i].GetId())
   375  		if self.iregions[i].GetId() == id {
   376  			return self.iregions[i], nil
   377  		}
   378  	}
   379  	return nil, cloudprovider.ErrNotFound
   380  }
   381  
   382  func (self *SHuaweiClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) {
   383  	for i := 0; i < len(self.iregions); i += 1 {
   384  		if self.iregions[i].GetGlobalId() == id {
   385  			return self.iregions[i], nil
   386  		}
   387  	}
   388  	return nil, cloudprovider.ErrNotFound
   389  }
   390  
   391  func (self *SHuaweiClient) GetRegion(regionId string) *SRegion {
   392  	if len(regionId) == 0 {
   393  		regionId = self.cpcfg.DefaultRegion
   394  	}
   395  
   396  	for i := 0; i < len(self.iregions); i += 1 {
   397  		if self.iregions[i].GetId() == regionId {
   398  			return self.iregions[i].(*SRegion)
   399  		}
   400  	}
   401  	return nil
   402  }
   403  
   404  func (self *SHuaweiClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) {
   405  	for i := 0; i < len(self.iregions); i += 1 {
   406  		ihost, err := self.iregions[i].GetIHostById(id)
   407  		if err == nil {
   408  			return ihost, nil
   409  		} else if errors.Cause(err) != cloudprovider.ErrNotFound {
   410  			return nil, err
   411  		}
   412  	}
   413  	return nil, cloudprovider.ErrNotFound
   414  }
   415  
   416  func (self *SHuaweiClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) {
   417  	for i := 0; i < len(self.iregions); i += 1 {
   418  		ivpc, err := self.iregions[i].GetIVpcById(id)
   419  		if err == nil {
   420  			return ivpc, nil
   421  		} else if errors.Cause(err) != cloudprovider.ErrNotFound {
   422  			return nil, err
   423  		}
   424  	}
   425  	return nil, cloudprovider.ErrNotFound
   426  }
   427  
   428  func (self *SHuaweiClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) {
   429  	for i := 0; i < len(self.iregions); i += 1 {
   430  		istorage, err := self.iregions[i].GetIStorageById(id)
   431  		if err == nil {
   432  			return istorage, nil
   433  		} else if errors.Cause(err) != cloudprovider.ErrNotFound {
   434  			return nil, err
   435  		}
   436  	}
   437  	return nil, cloudprovider.ErrNotFound
   438  }
   439  
   440  // 总账户余额
   441  type SAccountBalance struct {
   442  	AvailableAmount  float64
   443  	CreditAmount     float64
   444  	DesignatedAmount float64
   445  }
   446  
   447  // 账户余额
   448  // https://support.huaweicloud.com/api-oce/zh-cn_topic_0109685133.html
   449  type SBalance struct {
   450  	Amount           float64 `json:"amount"`
   451  	Currency         string  `json:"currency"`
   452  	AccountID        string  `json:"account_id"`
   453  	AccountType      int64   `json:"account_type"`
   454  	DesignatedAmount float64 `json:"designated_amount,omitempty"`
   455  	CreditAmount     float64 `json:"credit_amount,omitempty"`
   456  	MeasureUnit      int64   `json:"measure_unit"`
   457  }
   458  
   459  func (self *SHuaweiClient) GetVersion() string {
   460  	return HUAWEI_API_VERSION
   461  }
   462  
   463  func (self *SHuaweiClient) GetAccessEnv() string {
   464  	return ""
   465  }
   466  
   467  func (self *SHuaweiClient) GetCapabilities() []string {
   468  	caps := []string{
   469  		cloudprovider.CLOUD_CAPABILITY_PROJECT,
   470  		cloudprovider.CLOUD_CAPABILITY_COMPUTE,
   471  		cloudprovider.CLOUD_CAPABILITY_NETWORK,
   472  		cloudprovider.CLOUD_CAPABILITY_EIP,
   473  		cloudprovider.CLOUD_CAPABILITY_LOADBALANCER,
   474  		// cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE,
   475  		cloudprovider.CLOUD_CAPABILITY_QUOTA + cloudprovider.READ_ONLY_SUFFIX,
   476  		cloudprovider.CLOUD_CAPABILITY_RDS,
   477  		cloudprovider.CLOUD_CAPABILITY_CACHE,
   478  		cloudprovider.CLOUD_CAPABILITY_EVENT,
   479  		cloudprovider.CLOUD_CAPABILITY_CLOUDID,
   480  		cloudprovider.CLOUD_CAPABILITY_SAML_AUTH,
   481  		cloudprovider.CLOUD_CAPABILITY_NAT,
   482  		cloudprovider.CLOUD_CAPABILITY_NAS,
   483  		cloudprovider.CLOUD_CAPABILITY_MODELARTES,
   484  	}
   485  	// huawei objectstore is shared across projects(subscriptions)
   486  	// to avoid multiple project access the same bucket
   487  	// only main project is allow to access objectstore bucket
   488  	if self.isMainProject {
   489  		caps = append(caps, cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE)
   490  	}
   491  	return caps
   492  }
   493  
   494  func (self *SHuaweiClient) GetUserId() (string, error) {
   495  	client, err := self.newGeneralAPIClient()
   496  	if err != nil {
   497  		return "", errors.Wrap(err, "SHuaweiClient.GetUserId.newGeneralAPIClient")
   498  	}
   499  
   500  	type cred struct {
   501  		UserId string `json:"user_id"`
   502  	}
   503  
   504  	ret := &cred{}
   505  	err = DoGet(client.Credentials.Get, self.accessKey, nil, ret)
   506  	if err != nil {
   507  		return "", errors.Wrap(err, "SHuaweiClient.GetUserId.DoGet")
   508  	}
   509  
   510  	return ret.UserId, nil
   511  }
   512  
   513  // owner id == domain_id == account id
   514  func (self *SHuaweiClient) GetOwnerId() (string, error) {
   515  	userId, err := self.GetUserId()
   516  	if err != nil {
   517  		return "", errors.Wrap(err, "SHuaweiClient.GetOwnerId.GetUserId")
   518  	}
   519  
   520  	client, err := self.newGeneralAPIClient()
   521  	if err != nil {
   522  		return "", errors.Wrap(err, "SHuaweiClient.GetOwnerId.newGeneralAPIClient")
   523  	}
   524  
   525  	type user struct {
   526  		DomainId   string `json:"domain_id"`
   527  		Name       string `json:"name"`
   528  		CreateTime string
   529  	}
   530  
   531  	ret := &user{}
   532  	err = DoGet(client.Users.Get, userId, nil, ret)
   533  	if err != nil {
   534  		return "", errors.Wrap(err, "SHuaweiClient.GetOwnerId.DoGet")
   535  	}
   536  	self.ownerName = ret.Name
   537  	// 2021-02-02 02:43:28.0
   538  	self.ownerCreateTime, _ = timeutils.ParseTimeStr(strings.TrimSuffix(ret.CreateTime, ".0"))
   539  	return ret.DomainId, nil
   540  }
   541  
   542  func (self *SHuaweiClient) GetSamlEntityId() string {
   543  	return fmt.Sprintf("auth.%s", self.endpoints.EndpointDomain)
   544  }
   545  
   546  func (self *SHuaweiClient) initOwner() error {
   547  	ownerId, err := self.GetOwnerId()
   548  	if err != nil {
   549  		return errors.Wrap(err, "SHuaweiClient.initOwner")
   550  	}
   551  
   552  	self.ownerId = ownerId
   553  	return nil
   554  }