yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/aws/aws.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 aws 16 17 import ( 18 "bytes" 19 "fmt" 20 "io/ioutil" 21 "net/http" 22 "net/url" 23 "strings" 24 "sync" 25 "time" 26 27 "github.com/aws/aws-sdk-go/aws" 28 sdk "github.com/aws/aws-sdk-go/aws" 29 "github.com/aws/aws-sdk-go/aws/awserr" 30 "github.com/aws/aws-sdk-go/aws/credentials" 31 "github.com/aws/aws-sdk-go/aws/credentials/stscreds" 32 "github.com/aws/aws-sdk-go/aws/session" 33 "github.com/aws/aws-sdk-go/service/ec2" 34 "github.com/aws/aws-sdk-go/service/elasticache" 35 "github.com/aws/aws-sdk-go/service/s3" 36 37 "yunion.io/x/log" 38 "yunion.io/x/pkg/errors" 39 40 api "yunion.io/x/cloudmux/pkg/apis/compute" 41 "yunion.io/x/cloudmux/pkg/cloudprovider" 42 "yunion.io/x/onecloud/pkg/httperrors" 43 ) 44 45 const ( 46 CLOUD_PROVIDER_AWS = api.CLOUD_PROVIDER_AWS 47 CLOUD_PROVIDER_AWS_CN = "AWS" 48 CLOUD_PROVIDER_AWS_EN = "AWS" 49 50 AWS_INTERNATIONAL_CLOUDENV = "InternationalCloud" 51 AWS_CHINA_CLOUDENV = "ChinaCloud" 52 53 AWS_INTERNATIONAL_DEFAULT_REGION = "us-west-1" 54 AWS_CHINA_DEFAULT_REGION = "cn-north-1" 55 AWS_API_VERSION = "2018-10-10" 56 57 AWS_GLOBAL_ARN_PREFIX = "arn:aws:iam::aws:policy/" 58 AWS_CHINA_ARN_PREFIX = "arn:aws-cn:iam::aws:policy/" 59 60 DEFAULT_S3_REGION_ID = "us-east-1" 61 62 DefaultAssumeRoleName = "OrganizationAccountAccessRole" 63 ) 64 65 var ( 66 DEBUG = false 67 ) 68 69 type AwsClientConfig struct { 70 cpcfg cloudprovider.ProviderConfig 71 72 accessUrl string // 服务区域 ChinaCloud | InternationalCloud 73 accessKey string 74 accessSecret string 75 accountId string 76 77 debug bool 78 79 assumeRoleName string 80 } 81 82 func NewAwsClientConfig(accessUrl, accessKey, accessSecret, accountId string) *AwsClientConfig { 83 cfg := &AwsClientConfig{ 84 accessUrl: accessUrl, 85 accessKey: accessKey, 86 accessSecret: accessSecret, 87 accountId: accountId, 88 } 89 return cfg 90 } 91 92 func (cfg *AwsClientConfig) CloudproviderConfig(cpcfg cloudprovider.ProviderConfig) *AwsClientConfig { 93 cfg.cpcfg = cpcfg 94 return cfg 95 } 96 97 func (cfg *AwsClientConfig) Debug(debug bool) *AwsClientConfig { 98 cfg.debug = debug 99 DEBUG = debug 100 return cfg 101 } 102 103 func (cfg *AwsClientConfig) SetAssumeRole(roleName string) *AwsClientConfig { 104 cfg.assumeRoleName = roleName 105 return cfg 106 } 107 108 func (cfg *AwsClientConfig) getAssumeRoleName() string { 109 if len(cfg.assumeRoleName) > 0 { 110 return cfg.assumeRoleName 111 } 112 return DefaultAssumeRoleName 113 } 114 115 type SAwsClient struct { 116 *AwsClientConfig 117 118 ownerId string 119 ownerName string 120 121 iregions []cloudprovider.ICloudRegion 122 iBuckets []cloudprovider.ICloudBucket 123 124 //sessions map[string]map[bool]*session.Session 125 sessions sync.Map // map[string]map[bool]*session.Session 126 } 127 128 func NewAwsClient(cfg *AwsClientConfig) (*SAwsClient, error) { 129 client := SAwsClient{ 130 AwsClientConfig: cfg, 131 } 132 _, err := client.fetchRegions() 133 if err != nil { 134 return nil, err 135 } 136 err = client.fetchOwnerId() 137 if err != nil { 138 return nil, errors.Wrap(err, "fetchOwnerId") 139 } 140 if client.debug { 141 log.Debugf("ownerId: %s ownerName: %s", client.ownerId, client.ownerName) 142 } 143 return &client, nil 144 } 145 146 func (cli *SAwsClient) getIamArn(arn string) string { 147 switch cli.GetAccessEnv() { 148 case api.CLOUD_ACCESS_ENV_AWS_GLOBAL: 149 return AWS_GLOBAL_ARN_PREFIX + arn 150 default: 151 return AWS_CHINA_ARN_PREFIX + arn 152 } 153 } 154 155 func (cli *SAwsClient) getIamCommonArn(arn string) string { 156 switch cli.GetAccessEnv() { 157 case api.CLOUD_ACCESS_ENV_AWS_GLOBAL: 158 return strings.TrimPrefix(arn, AWS_GLOBAL_ARN_PREFIX) 159 default: 160 return strings.TrimPrefix(arn, AWS_CHINA_ARN_PREFIX) 161 } 162 } 163 164 func GetDefaultRegionId(accessUrl string) string { 165 defaultRegion := AWS_INTERNATIONAL_DEFAULT_REGION 166 switch accessUrl { 167 case AWS_INTERNATIONAL_CLOUDENV: 168 defaultRegion = AWS_INTERNATIONAL_DEFAULT_REGION 169 case AWS_CHINA_CLOUDENV: 170 defaultRegion = AWS_CHINA_DEFAULT_REGION 171 } 172 173 return defaultRegion 174 } 175 176 func (self *SAwsClient) getDefaultRegionId() string { 177 return GetDefaultRegionId(self.accessUrl) 178 } 179 180 func (client *SAwsClient) getDefaultSession(assumeRole bool) (*session.Session, error) { 181 return client.getAwsSession(client.getDefaultRegionId(), assumeRole) 182 } 183 184 func (client *SAwsClient) GetAccountId() string { 185 return client.ownerId 186 } 187 188 var ( 189 // cache for describeRegions 190 describeRegionResult sync.Map //map[string]*ec2.DescribeRegionsOutput = map[string]*ec2.DescribeRegionsOutput{} 191 describeRegionResultCacheAt sync.Map // map[string]time.Time = map[string]time.Time{} 192 ) 193 194 const ( 195 describeRegionExpireHours = 2 196 ) 197 198 // 用于初始化region信息 199 func (self *SAwsClient) fetchRegions() ([]SRegion, error) { 200 cacheTime, _ := describeRegionResultCacheAt.Load(self.accessUrl) 201 if _, ok := describeRegionResult.Load(self.accessUrl); !ok || cacheTime.(time.Time).IsZero() || time.Now().After(cacheTime.(time.Time).Add(time.Hour*describeRegionExpireHours)) { 202 s, err := self.getDefaultSession(false) 203 if err != nil { 204 return nil, errors.Wrap(err, "getDefaultSession") 205 } 206 svc := ec2.New(s) 207 // https://docs.aws.amazon.com/sdk-for-go/api/service/ec2/#EC2.DescribeRegions 208 input := &ec2.DescribeRegionsInput{} 209 input.SetAllRegions(true) 210 result, err := svc.DescribeRegions(input) 211 if err != nil { 212 if e, ok := err.(awserr.Error); ok && e.Code() == "AuthFailure" { 213 return nil, errors.Wrap(httperrors.ErrInvalidAccessKey, err.Error()) 214 } 215 return nil, errors.Wrap(err, "DescribeRegions") 216 } 217 describeRegionResult.Store(self.accessUrl, result) 218 describeRegionResultCacheAt.Store(self.accessUrl, time.Now()) 219 } 220 221 self.iregions = []cloudprovider.ICloudRegion{} 222 regions := make([]SRegion, 0) 223 descRegions, _ := describeRegionResult.Load(self.accessUrl) 224 for _, region := range descRegions.(*ec2.DescribeRegionsOutput).Regions { 225 name := *region.RegionName 226 endpoint := *region.Endpoint 227 sregion := SRegion{client: self, RegionId: name, RegionEndpoint: endpoint} 228 // 初始化region client 229 // sregion.getEc2Client() 230 regions = append(regions, sregion) 231 self.iregions = append(self.iregions, &sregion) 232 } 233 234 return regions, nil 235 } 236 237 func (client *SAwsClient) getAwsSession(regionId string, assumeRole bool) (*session.Session, error) { 238 client.sessions.LoadOrStore(regionId, map[bool]*session.Session{}) 239 if sess, ok := client.sessions.Load(regionId); ok { 240 if ret, ok := sess.(map[bool]*session.Session)[assumeRole]; ok { 241 return ret, nil 242 } 243 } 244 httpClient := client.cpcfg.AdaptiveTimeoutHttpClient() 245 transport, _ := httpClient.Transport.(*http.Transport) 246 httpClient.Transport = cloudprovider.GetCheckTransport(transport, func(req *http.Request) (func(resp *http.Response), error) { 247 var action string 248 if req.ContentLength > 0 { 249 body, err := ioutil.ReadAll(req.Body) 250 if err != nil { 251 return nil, errors.Wrapf(err, "ioutil.ReadAll") 252 } 253 req.Body = ioutil.NopCloser(bytes.NewBuffer(body)) 254 params, err := url.ParseQuery(string(body)) 255 if err != nil { 256 return nil, errors.Wrapf(err, "ParseQuery(%s)", string(body)) 257 } 258 action = params.Get("Action") 259 } 260 261 service := strings.Split(req.URL.Host, ".")[0] 262 method, path := req.Method, req.URL.Path 263 respCheck := func(resp *http.Response) { 264 if resp.StatusCode == 403 { 265 if client.cpcfg.UpdatePermission != nil { 266 if len(action) > 0 { 267 client.cpcfg.UpdatePermission(service, action) 268 } else { // s3 269 client.cpcfg.UpdatePermission(service, fmt.Sprintf("%s %s", method, path)) 270 } 271 } 272 } 273 } 274 275 if client.cpcfg.ReadOnly { 276 if len(action) > 0 { 277 for _, prefix := range []string{"Get", "List", "Describe"} { 278 if strings.HasPrefix(action, prefix) { 279 return respCheck, nil 280 } 281 } 282 return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, action) 283 } 284 // s3 285 if req.Method == "GET" || req.Method == "HEAD" { 286 return respCheck, nil 287 } 288 return nil, errors.Wrapf(cloudprovider.ErrAccountReadOnly, "%s %s", req.Method, req.URL.Path) 289 } 290 return respCheck, nil 291 }) 292 s, err := session.NewSession(&sdk.Config{ 293 Region: sdk.String(regionId), 294 Credentials: credentials.NewStaticCredentials( 295 client.accessKey, client.accessSecret, "", 296 ), 297 HTTPClient: httpClient, 298 DisableParamValidation: sdk.Bool(true), 299 CredentialsChainVerboseErrors: sdk.Bool(true), 300 }) 301 if err != nil { 302 return nil, errors.Wrap(err, "getAwsSession.NewSession") 303 } 304 if assumeRole && len(client.accountId) > 0 { 305 // need to assumeRole 306 var env string 307 switch client.GetAccessEnv() { 308 case api.CLOUD_ACCESS_ENV_AWS_GLOBAL: 309 env = "aws" 310 default: 311 env = "aws-cn" 312 } 313 roleARN := fmt.Sprintf("arn:%s:iam::%s:role/%s", env, client.accountId, client.getAssumeRoleName()) 314 creds := stscreds.NewCredentials(s, roleARN) 315 s = s.Copy(&aws.Config{Credentials: creds}) 316 } 317 if client.debug { 318 logLevel := aws.LogLevelType(uint(aws.LogDebugWithRequestErrors) + uint(aws.LogDebugWithHTTPBody) + uint(aws.LogDebugWithSigning)) 319 s.Config.LogLevel = &logLevel 320 } 321 322 client.sessions.Store(regionId, map[bool]*session.Session{assumeRole: s}) 323 return s, nil 324 } 325 326 func (region *SRegion) getAwsElasticacheClient() (*elasticache.ElastiCache, error) { 327 session, err := region.getAwsSession() 328 if err != nil { 329 return nil, errors.Wrap(err, "client.getDefaultSession") 330 } 331 session.ClientConfig(ELASTICACHE_SERVICE_NAME) 332 return elasticache.New(session), nil 333 } 334 335 func (client *SAwsClient) getAwsRoute53Session() (*session.Session, error) { 336 session, err := client.getDefaultSession(true) 337 if err != nil { 338 return nil, errors.Wrap(err, "client.getDefaultSession()") 339 } 340 session.ClientConfig(ROUTE53_SERVICE_NAME) 341 return session, nil 342 } 343 344 func (self *SAwsClient) invalidateIBuckets() { 345 self.iBuckets = nil 346 } 347 348 func (self *SAwsClient) getIBuckets() ([]cloudprovider.ICloudBucket, error) { 349 if self.iBuckets == nil { 350 err := self.fetchBuckets() 351 if err != nil { 352 return nil, errors.Wrap(err, "fetchBuckets") 353 } 354 } 355 return self.iBuckets, nil 356 } 357 358 func (client *SAwsClient) fetchOwnerId() error { 359 ident, err := client.GetCallerIdentity() 360 if err != nil { 361 return errors.Wrap(err, "GetCallerIdentity") 362 } 363 client.ownerId = ident.Account 364 return nil 365 } 366 367 func (client *SAwsClient) fetchBuckets() error { 368 s, err := client.getDefaultSession(true) 369 if err != nil { 370 return errors.Wrap(err, "getDefaultSession") 371 } 372 s3cli := s3.New(s) 373 output, err := s3cli.ListBuckets(&s3.ListBucketsInput{}) 374 if err != nil { 375 return errors.Wrap(err, "ListBuckets") 376 } 377 378 ret := make([]cloudprovider.ICloudBucket, 0) 379 for _, bInfo := range output.Buckets { 380 if err := FillZero(bInfo); err != nil { 381 log.Errorf("s3cli.Binfo.FillZero error %s", err) 382 continue 383 } 384 385 input := &s3.GetBucketLocationInput{} 386 input.Bucket = bInfo.Name 387 output, err := s3cli.GetBucketLocation(input) 388 if err != nil { 389 log.Errorf("s3cli.GetBucketLocation error %s", err) 390 continue 391 } 392 393 if err := FillZero(output); err != nil { 394 log.Errorf("s3cli.GetBucketLocation.FillZero error %s", err) 395 continue 396 } 397 398 location := *output.LocationConstraint 399 if len(location) == 0 { 400 // https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketLocation.html 401 // Buckets in Region us-east-1 have a LocationConstraint of null. 402 location = DEFAULT_S3_REGION_ID 403 } 404 region, err := client.getIRegionByRegionId(location) 405 if err != nil { 406 log.Errorf("client.getIRegionByRegionId %s fail %s", location, err) 407 continue 408 } 409 b := SBucket{ 410 region: region.(*SRegion), 411 Name: *bInfo.Name, 412 Location: location, 413 CreationDate: *bInfo.CreationDate, 414 } 415 ret = append(ret, &b) 416 } 417 418 client.iBuckets = ret 419 420 return nil 421 } 422 423 // 只是使用fetchRegions初始化好的self.iregions. 本身并不从云服务器厂商拉取region信息 424 func (self *SAwsClient) GetRegions() ([]SRegion, error) { 425 return self.fetchRegions() 426 } 427 428 func (self *SAwsClient) GetIRegions() []cloudprovider.ICloudRegion { 429 return self.iregions 430 } 431 432 func (self *SAwsClient) GetRegion(regionId string) (*SRegion, error) { 433 regions, err := self.fetchRegions() 434 if err != nil { 435 return nil, errors.Wrapf(err, "fetchRegions") 436 } 437 438 if len(regionId) == 0 { 439 regionId = AWS_INTERNATIONAL_DEFAULT_REGION 440 switch self.accessUrl { 441 case AWS_INTERNATIONAL_CLOUDENV: 442 regionId = AWS_INTERNATIONAL_DEFAULT_REGION 443 case AWS_CHINA_CLOUDENV: 444 regionId = AWS_CHINA_DEFAULT_REGION 445 } 446 } 447 for i := 0; i < len(regions); i += 1 { 448 if regions[i].GetId() == regionId { 449 return ®ions[i], nil 450 } 451 } 452 return nil, errors.Wrap(cloudprovider.ErrNotFound, regionId) 453 } 454 455 func (self *SAwsClient) getDefaultRegion() (*SRegion, error) { 456 return self.GetRegion("") 457 } 458 459 func (self *SAwsClient) getIRegionByRegionId(id string) (cloudprovider.ICloudRegion, error) { 460 for i := 0; i < len(self.iregions); i += 1 { 461 if self.iregions[i].GetId() == id { 462 return self.iregions[i], nil 463 } 464 } 465 return nil, errors.Wrap(cloudprovider.ErrNotFound, "getIRegionByRegionId") 466 } 467 468 func (self *SAwsClient) GetIRegionById(id string) (cloudprovider.ICloudRegion, error) { 469 for i := 0; i < len(self.iregions); i += 1 { 470 if self.iregions[i].GetGlobalId() == id { 471 return self.iregions[i], nil 472 } 473 } 474 return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetIRegionById") 475 } 476 477 func (self *SAwsClient) GetIHostById(id string) (cloudprovider.ICloudHost, error) { 478 for i := 0; i < len(self.iregions); i += 1 { 479 ihost, err := self.iregions[i].GetIHostById(id) 480 if err == nil { 481 return ihost, nil 482 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 483 log.Errorf("GetIHostById %s: %s", id, err) 484 return nil, errors.Wrap(err, "GetIHostById") 485 } 486 } 487 return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetIHostById") 488 } 489 490 func (self *SAwsClient) GetIVpcById(id string) (cloudprovider.ICloudVpc, error) { 491 for i := 0; i < len(self.iregions); i += 1 { 492 ihost, err := self.iregions[i].GetIVpcById(id) 493 if err == nil { 494 return ihost, nil 495 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 496 log.Errorf("GetIVpcById %s: %s", id, err) 497 return nil, errors.Wrap(err, "GetIVpcById") 498 } 499 } 500 return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetIVpcById") 501 } 502 503 func (self *SAwsClient) GetIStorageById(id string) (cloudprovider.ICloudStorage, error) { 504 for i := 0; i < len(self.iregions); i += 1 { 505 ihost, err := self.iregions[i].GetIStorageById(id) 506 if err == nil { 507 return ihost, nil 508 } else if errors.Cause(err) != cloudprovider.ErrNotFound { 509 log.Errorf("GetIStorageById %s: %s", id, err) 510 return nil, errors.Wrap(err, "GetIStorageById") 511 } 512 } 513 return nil, errors.Wrap(cloudprovider.ErrNotFound, "GetIStorageById") 514 } 515 516 type SAccountBalance struct { 517 AvailableAmount float64 518 AvailableCashAmount float64 519 CreditAmount float64 520 MybankCreditAmount float64 521 Currency string 522 } 523 524 func (self *SAwsClient) QueryAccountBalance() (*SAccountBalance, error) { 525 return nil, cloudprovider.ErrNotSupported 526 } 527 528 func (self *SAwsClient) GetIProjects() ([]cloudprovider.ICloudProject, error) { 529 return nil, cloudprovider.ErrNotImplemented 530 } 531 532 func (self *SAwsClient) GetAccessEnv() string { 533 switch self.accessUrl { 534 case AWS_INTERNATIONAL_CLOUDENV: 535 return api.CLOUD_ACCESS_ENV_AWS_GLOBAL 536 case AWS_CHINA_CLOUDENV: 537 return api.CLOUD_ACCESS_ENV_AWS_CHINA 538 default: 539 return api.CLOUD_ACCESS_ENV_AWS_GLOBAL 540 } 541 } 542 543 func (self *SAwsClient) iamRequest(apiName string, params map[string]string, retval interface{}) error { 544 return self.request("", IAM_SERVICE_NAME, IAM_SERVICE_ID, "2010-05-08", apiName, params, retval, true) 545 } 546 547 func (self *SAwsClient) stsRequest(apiName string, params map[string]string, retval interface{}) error { 548 return self.request("", STS_SERVICE_NAME, STS_SERVICE_ID, "2011-06-15", apiName, params, retval, false) 549 } 550 551 func (self *SAwsClient) GetCapabilities() []string { 552 caps := []string{ 553 // cloudprovider.CLOUD_CAPABILITY_PROJECT, 554 cloudprovider.CLOUD_CAPABILITY_COMPUTE, 555 cloudprovider.CLOUD_CAPABILITY_NETWORK, 556 cloudprovider.CLOUD_CAPABILITY_EIP, 557 cloudprovider.CLOUD_CAPABILITY_LOADBALANCER, 558 cloudprovider.CLOUD_CAPABILITY_OBJECTSTORE, 559 cloudprovider.CLOUD_CAPABILITY_RDS, 560 cloudprovider.CLOUD_CAPABILITY_CACHE + cloudprovider.READ_ONLY_SUFFIX, 561 cloudprovider.CLOUD_CAPABILITY_NAT + cloudprovider.READ_ONLY_SUFFIX, 562 cloudprovider.CLOUD_CAPABILITY_EVENT, 563 cloudprovider.CLOUD_CAPABILITY_CLOUDID, 564 cloudprovider.CLOUD_CAPABILITY_DNSZONE, 565 cloudprovider.CLOUD_CAPABILITY_SAML_AUTH, 566 cloudprovider.CLOUD_CAPABILITY_WAF, 567 } 568 return caps 569 } 570 571 func (client *SAwsClient) GetIamLoginUrl() string { 572 identity, err := client.GetCallerIdentity() 573 if err != nil { 574 log.Errorf("failed to get caller identity error: %v", err) 575 return "" 576 } 577 578 switch client.GetAccessEnv() { 579 case api.CLOUD_ACCESS_ENV_AWS_CHINA: 580 return fmt.Sprintf("https://%s.signin.amazonaws.cn/console/", identity.Account) 581 default: 582 return fmt.Sprintf("https://%s.signin.aws.amazon.com/console/", identity.Account) 583 } 584 } 585 586 func (client *SAwsClient) GetBucketCannedAcls() []string { 587 switch client.GetAccessEnv() { 588 case api.CLOUD_ACCESS_ENV_AWS_CHINA: 589 return []string{ 590 string(cloudprovider.ACLPrivate), 591 } 592 default: 593 return []string{ 594 string(cloudprovider.ACLPrivate), 595 string(cloudprovider.ACLAuthRead), 596 string(cloudprovider.ACLPublicRead), 597 string(cloudprovider.ACLPublicReadWrite), 598 } 599 } 600 } 601 602 func (client *SAwsClient) GetObjectCannedAcls() []string { 603 switch client.GetAccessEnv() { 604 case api.CLOUD_ACCESS_ENV_AWS_CHINA: 605 return []string{ 606 string(cloudprovider.ACLPrivate), 607 } 608 default: 609 return []string{ 610 string(cloudprovider.ACLPrivate), 611 string(cloudprovider.ACLAuthRead), 612 string(cloudprovider.ACLPublicRead), 613 string(cloudprovider.ACLPublicReadWrite), 614 } 615 } 616 } 617 618 func (client *SAwsClient) GetSamlEntityId() string { 619 switch client.accessUrl { 620 case AWS_CHINA_CLOUDENV: 621 return cloudprovider.SAML_ENTITY_ID_AWS_CN 622 default: 623 return cloudprovider.SAML_ENTITY_ID_AWS 624 } 625 }