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(®ions, "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] = ®ions[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 }