yunion.io/x/cloudmux@v0.3.10-0-alpha.1/pkg/multicloud/aws/storagecache.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 "context" 19 "fmt" 20 "strings" 21 "time" 22 23 "github.com/aws/aws-sdk-go/service/ec2" 24 "github.com/aws/aws-sdk-go/service/iam" 25 "github.com/aws/aws-sdk-go/service/s3" 26 27 "yunion.io/x/jsonutils" 28 "yunion.io/x/log" 29 "yunion.io/x/pkg/errors" 30 31 "yunion.io/x/cloudmux/pkg/cloudprovider" 32 "yunion.io/x/onecloud/pkg/compute/options" 33 "yunion.io/x/onecloud/pkg/mcclient" 34 "yunion.io/x/onecloud/pkg/mcclient/auth" 35 modules "yunion.io/x/onecloud/pkg/mcclient/modules/image" 36 "yunion.io/x/cloudmux/pkg/multicloud" 37 "yunion.io/x/onecloud/pkg/util/qemuimg" 38 ) 39 40 type SStoragecache struct { 41 multicloud.SResourceBase 42 AwsTags 43 region *SRegion 44 } 45 46 func (self *SStoragecache) GetId() string { 47 return fmt.Sprintf("%s-%s", self.region.client.cpcfg.Id, self.region.GetId()) 48 } 49 50 func (self *SStoragecache) GetName() string { 51 return fmt.Sprintf("%s-%s", self.region.client.cpcfg.Name, self.region.GetId()) 52 } 53 54 func (self *SStoragecache) GetGlobalId() string { 55 return fmt.Sprintf("%s-%s", self.region.client.cpcfg.Id, self.region.GetGlobalId()) 56 } 57 58 func (self *SStoragecache) GetStatus() string { 59 return "available" 60 } 61 62 func (self *SStoragecache) Refresh() error { 63 return nil 64 } 65 66 func (self *SStoragecache) IsEmulated() bool { 67 return false 68 } 69 70 func (self *SStoragecache) GetICustomizedCloudImages() ([]cloudprovider.ICloudImage, error) { 71 images, err := self.region.GetImages("", ImageOwnerSelf, nil, "", "hvm", nil, "", true) 72 if err != nil { 73 return nil, errors.Wrapf(err, "GetImages") 74 } 75 ret := []cloudprovider.ICloudImage{} 76 for i := 0; i < len(images); i += 1 { 77 images[i].storageCache = self 78 ret = append(ret, &images[i]) 79 } 80 return ret, nil 81 } 82 83 func (self *SStoragecache) GetICloudImages() ([]cloudprovider.ICloudImage, error) { 84 return nil, cloudprovider.ErrNotImplemented 85 } 86 87 func (self *SStoragecache) GetIImageById(extId string) (cloudprovider.ICloudImage, error) { 88 if len(extId) == 0 { 89 return nil, fmt.Errorf("GetIImageById image id should not be empty") 90 } 91 92 part, err := self.region.GetImage(extId) 93 if err != nil { 94 log.Errorf("GetImage %s %s", extId, err) 95 return nil, errors.Wrap(err, "GetImage") 96 } 97 part.storageCache = self 98 return part, nil 99 } 100 101 func (self *SStoragecache) GetPath() string { 102 return "" 103 } 104 105 func (self *SStoragecache) CreateIImage(snapshotId, imageName, osType, imageDesc string) (cloudprovider.ICloudImage, error) { 106 imageId, err := self.region.createIImage(snapshotId, imageName, imageDesc) 107 if err != nil { 108 log.Errorf("createIImage %s %s %s: %s", snapshotId, imageName, imageDesc, err) 109 return nil, errors.Wrap(err, "createIImage") 110 } 111 image, err := self.region.GetImage(imageId) 112 if err != nil { 113 log.Errorf("GetImage %s: %s", imageId, err) 114 return nil, errors.Wrap(err, "GetImage") 115 } 116 image.storageCache = self 117 iimage := make([]cloudprovider.ICloudImage, 1) 118 iimage[0] = image 119 //todo : implement me 120 if err := cloudprovider.WaitStatus(iimage[0], "avaliable", 15*time.Second, 3600*time.Second); err != nil { 121 return nil, errors.Wrap(err, "WaitStatus.iimage") 122 } 123 return iimage[0], nil 124 } 125 126 func (self *SStoragecache) DownloadImage(userCred mcclient.TokenCredential, imageId string, extId string, path string) (jsonutils.JSONObject, error) { 127 return self.downloadImage(userCred, imageId, extId) 128 } 129 130 func (self *SStoragecache) UploadImage(ctx context.Context, userCred mcclient.TokenCredential, image *cloudprovider.SImageCreateOption, callback func(progress float32)) (string, error) { 131 return self.uploadImage(ctx, userCred, image, callback) 132 133 } 134 135 func (self *SStoragecache) uploadImage(ctx context.Context, userCred mcclient.TokenCredential, image *cloudprovider.SImageCreateOption, callback func(progress float32)) (string, error) { 136 err := self.region.initVmimport() 137 if err != nil { 138 return "", errors.Wrap(err, "initVmimport") 139 } 140 141 bucketName := GetBucketName(self.region.GetId(), image.ImageId) 142 143 exist, err := self.region.IBucketExist(bucketName) 144 if err != nil { 145 return "", errors.Wrap(err, "IBucketExist") 146 } 147 148 if !exist { 149 err = self.region.CreateIBucket(bucketName, "", "") 150 if err != nil { 151 return "", errors.Wrap(err, "CreateIBucket") 152 } 153 } 154 155 defer self.region.DeleteIBucket(bucketName) 156 157 s := auth.GetAdminSession(ctx, options.Options.Region) 158 meta, reader, sizeBytes, err := modules.Images.Download(s, image.ImageId, string(qemuimg.VMDK), false) 159 if err != nil { 160 return "", errors.Wrap(err, "Images.Download") 161 } 162 log.Debugf("Images meta data %s", meta) 163 164 diskFormat, _ := meta.GetString("disk_format") 165 166 bucket, err := self.region.GetIBucketByName(bucketName) 167 if err != nil { 168 return "", errors.Wrap(err, "GetIBucketByName") 169 } 170 body := multicloud.NewProgress(sizeBytes, 80, reader, callback) 171 err = cloudprovider.UploadObject(ctx, bucket, image.ImageId, 0, body, sizeBytes, "", "", nil, false) 172 if err != nil { 173 return "", errors.Wrap(err, "cloudprovider.UploadObject") 174 } 175 176 defer bucket.DeleteObject(ctx, image.ImageId) 177 178 imageBaseName := image.ImageId 179 if imageBaseName[0] >= '0' && imageBaseName[0] <= '9' { 180 imageBaseName = fmt.Sprintf("img%s", image.ImageId) 181 } 182 imageName := imageBaseName 183 nameIdx := 1 184 185 // check image name, avoid name conflict 186 for { 187 _, err = self.region.GetImageByName(imageName, ImageOwnerSelf) 188 if err != nil { 189 if errors.Cause(err) == cloudprovider.ErrNotFound { 190 break 191 } else { 192 return "", err 193 } 194 } 195 196 imageName = fmt.Sprintf("%s-%d", imageBaseName, nameIdx) 197 nameIdx += 1 198 log.Debugf("uploadImage Match remote name %s", imageName) 199 } 200 201 task, err := self.region.ImportImage(imageName, image.OsArch, image.OsType, image.OsDistribution, diskFormat, bucketName, image.ImageId) 202 203 if err != nil { 204 log.Errorf("ImportImage error %s %s %s", image.ImageId, bucketName, err) 205 return "", err 206 } 207 208 err = cloudprovider.Wait(2*time.Minute, 4*time.Hour, func() (bool, error) { 209 status := task.GetStatus() 210 if status == ImageImportStatusDeleted { 211 return false, errors.Wrap(errors.ErrInvalidStatus, "SStoragecache.ImageImportStatusDeleted") 212 } 213 214 if status == ImageImportStatusCompleted { 215 return true, nil 216 } 217 218 return false, nil 219 }) 220 if err != nil { 221 return "", errors.Wrap(err, "SStoragecache.Wait") 222 } 223 224 // add name tag 225 self.region.addTags(task.ImageId, "Name", image.ImageId) 226 if callback != nil { 227 callback(100) 228 } 229 return task.ImageId, nil 230 } 231 232 func (self *SStoragecache) downloadImage(userCred mcclient.TokenCredential, imageId string, extId string) (jsonutils.JSONObject, error) { 233 // aws 导出镜像限制比较多。https://docs.aws.amazon.com/zh_cn/vm-import/latest/userguide/vmexport.html 234 bucketName := GetBucketName(self.region.GetId(), imageId) 235 if err := self.region.checkBucket(bucketName); err != nil { 236 log.Errorf("checkBucket %s: %s", bucketName, err) 237 return nil, errors.Wrap(err, "checkBucket") 238 } 239 240 instanceId, err := self.region.GetInstanceIdByImageId(extId) 241 if err != nil { 242 log.Errorf("GetInstanceIdByImageId %s: %s", extId, err) 243 return nil, errors.Wrap(err, "GetInstanceIdByImageId") 244 } 245 246 ec2Client, err := self.region.getEc2Client() 247 if err != nil { 248 return nil, errors.Wrap(err, "getEc2Client") 249 } 250 251 task, err := self.region.ExportImage(instanceId, imageId) 252 if err != nil { 253 log.Errorf("ExportImage %s %s: %s", instanceId, imageId, err) 254 return nil, errors.Wrap(err, "ExportImage") 255 } 256 257 taskParams := &ec2.DescribeExportTasksInput{} 258 taskParams.SetExportTaskIds([]*string{&task.TaskId}) 259 if err := ec2Client.WaitUntilExportTaskCompleted(taskParams); err != nil { 260 log.Errorf("WaitUntilExportTaskCompleted %#v %s", taskParams, err) 261 return nil, errors.Wrap(err, "WaitUntilExportTaskCompleted") 262 } 263 264 s3Client, err := self.region.GetS3Client() 265 if err != nil { 266 return nil, errors.Wrap(err, "GetS3Client") 267 } 268 269 i := &s3.GetObjectInput{} 270 i.SetBucket(bucketName) 271 i.SetKey(fmt.Sprintf("%s.%s", task.TaskId, "ova")) 272 ret, err := s3Client.GetObject(i) 273 if err != nil { 274 log.Errorf("GetObject %#v: %s", i, err) 275 return nil, errors.Wrap(err, "GetObject") 276 } 277 278 s := auth.GetAdminSession(context.Background(), options.Options.Region) 279 params := jsonutils.Marshal(map[string]string{"image_id": imageId, "disk-format": "raw"}) 280 if result, err := modules.Images.Upload(s, params, ret.Body, IntVal(ret.ContentLength)); err != nil { 281 return nil, errors.Wrap(err, "Images.Images") 282 } else { 283 return result, nil 284 } 285 } 286 287 func (self *SRegion) CheckBucket(bucketName string) error { 288 return self.checkBucket(bucketName) 289 } 290 291 func (self *SRegion) checkBucket(bucketName string) error { 292 exists, err := self.IsBucketExist(bucketName) 293 if err != nil { 294 log.Errorf("IsBucketExist %s: %s", bucketName, err) 295 return errors.Wrap(err, "IsBucketExist") 296 } 297 298 if !exists { 299 return fmt.Errorf("bucket %s not found", bucketName) 300 } else { 301 return nil 302 } 303 304 } 305 306 func (self *SRegion) IsBucketExist(bucketName string) (bool, error) { 307 s3Client, err := self.GetS3Client() 308 if err != nil { 309 return false, errors.Wrap(err, "IsBucketExist.GetS3Client") 310 } 311 312 params := &s3.ListBucketsInput{} 313 ret, err := s3Client.ListBuckets(params) 314 if err != nil { 315 return false, errors.Wrap(err, "ListBuckets") 316 } 317 318 for _, bucket := range ret.Buckets { 319 if bucket.Name != nil && *bucket.Name == bucketName { 320 return true, nil 321 } 322 } 323 324 return false, nil 325 } 326 327 func (self *SRegion) GetBucketRegionId(bucketName string) (string, error) { 328 s3Client, err := self.GetS3Client() 329 if err != nil { 330 return "", err 331 } 332 333 params := &s3.GetBucketLocationInput{Bucket: &bucketName} 334 ret, err := s3Client.GetBucketLocation(params) 335 if err != nil { 336 return "", err 337 } 338 339 return StrVal(ret.LocationConstraint), nil 340 } 341 342 func (self *SRegion) GetARNPartition() string { 343 // https://docs.amazonaws.cn/general/latest/gr/aws-arns-and-namespaces.html?id=docs_gateway 344 // https://github.com/aws/chalice/issues/777 345 // https://github.com/aws/chalice/issues/792 346 /* 347 I assume this is because the ARN format is slightly different for China. 348 In general, ARNs follow the pattern arn:partition:service:region:account-id:resource, 349 where partition is aws for most of the world and aws-cn for China. 350 It looks like the more common "arn:aws" is currently hardcoded in quite a few places. 351 */ 352 if strings.HasPrefix(self.RegionId, "cn-") { 353 return "aws-cn" 354 } else { 355 return "aws" 356 } 357 } 358 359 func (self *SRegion) initVmimportRole() error { 360 /*需要api access token 具备iam Full access权限*/ 361 iamClient, err := self.getIamClient() 362 if err != nil { 363 return err 364 } 365 366 // search role vmimport 367 rolename := "vmimport" 368 ret, _ := iamClient.GetRole(&iam.GetRoleInput{RoleName: &rolename}) 369 // todo: 这里得区分是not found.还是其他错误 370 if ret.Role != nil && ret.Role.RoleId != nil { 371 return nil 372 } else { 373 // create it 374 roleDoc := `{ 375 "Version": "2012-10-17", 376 "Statement": [ 377 { 378 "Effect": "Allow", 379 "Principal": { "Service": "vmie.amazonaws.com" }, 380 "Action": "sts:AssumeRole", 381 "Condition": { 382 "StringEquals":{ 383 "sts:Externalid": "vmimport" 384 } 385 } 386 } 387 ] 388 }` 389 params := &iam.CreateRoleInput{} 390 params.SetDescription("vmimport role for image import") 391 params.SetRoleName(rolename) 392 params.SetAssumeRolePolicyDocument(roleDoc) 393 394 _, err = iamClient.CreateRole(params) 395 return err 396 } 397 } 398 399 func (self *SRegion) initVmimportRolePolicy() error { 400 /*需要api access token 具备iam Full access权限*/ 401 iamClient, err := self.getIamClient() 402 if err != nil { 403 return err 404 } 405 406 partition := self.GetARNPartition() 407 roleName := "vmimport" 408 policyName := "vmimport" 409 ret, err := iamClient.GetRolePolicy(&iam.GetRolePolicyInput{RoleName: &roleName, PolicyName: &policyName}) 410 // todo: 这里得区分是not found.还是其他错误. 411 if ret.PolicyName != nil { 412 return nil 413 } else { 414 rolePolicy := `{ 415 "Version":"2012-10-17", 416 "Statement":[ 417 { 418 "Effect":"Allow", 419 "Action":[ 420 "s3:GetBucketLocation", 421 "s3:GetObject", 422 "s3:ListBucket" 423 ], 424 "Resource":[ 425 "arn:%[1]s:s3:::%[2]s", 426 "arn:%[1]s:s3:::%[2]s/*" 427 ] 428 }, 429 { 430 "Effect":"Allow", 431 "Action":[ 432 "ec2:ModifySnapshotAttribute", 433 "ec2:CopySnapshot", 434 "ec2:RegisterImage", 435 "ec2:Describe*" 436 ], 437 "Resource":"*" 438 } 439 ] 440 }` 441 params := &iam.PutRolePolicyInput{} 442 params.SetPolicyDocument(fmt.Sprintf(rolePolicy, partition, "imgcache-*")) 443 params.SetPolicyName(policyName) 444 params.SetRoleName(roleName) 445 _, err = iamClient.PutRolePolicy(params) 446 return err 447 } 448 } 449 450 func (self *SRegion) initVmimport() error { 451 if err := self.initVmimportRole(); err != nil { 452 return err 453 } 454 455 if err := self.initVmimportRolePolicy(); err != nil { 456 return err 457 } 458 459 return nil 460 } 461 462 func (self *SRegion) createIImage(snapshotId, imageName, imageDesc string) (string, error) { 463 params := &ec2.CreateImageInput{} 464 params.SetDescription(imageDesc) 465 params.SetName(imageName) 466 block := &ec2.BlockDeviceMapping{} 467 block.SetDeviceName("/dev/sda1") 468 ebs := &ec2.EbsBlockDevice{} 469 ebs.SetSnapshotId(snapshotId) 470 ebs.SetDeleteOnTermination(true) 471 block.SetEbs(ebs) 472 blockList := []*ec2.BlockDeviceMapping{block} 473 params.SetBlockDeviceMappings(blockList) 474 475 ec2Client, err := self.getEc2Client() 476 if err != nil { 477 return "", errors.Wrap(err, "getEc2Client") 478 } 479 ret, err := ec2Client.CreateImage(params) 480 if err != nil { 481 return "", err 482 } 483 return *ret.ImageId, nil 484 } 485 486 func (self *SRegion) getStoragecache() *SStoragecache { 487 if self.storageCache == nil { 488 self.storageCache = &SStoragecache{region: self} 489 } 490 return self.storageCache 491 } 492 493 func (region *SRegion) GetIStoragecaches() ([]cloudprovider.ICloudStoragecache, error) { 494 storageCache := region.getStoragecache() 495 return []cloudprovider.ICloudStoragecache{storageCache}, nil 496 }