package alicloudimport import ( "context" "fmt" "log" "strconv" "strings" "time" "github.com/aliyun/aliyun-oss-go-sdk/oss" packercommon "github.com/denverdino/aliyungo/common" "github.com/denverdino/aliyungo/ecs" "github.com/denverdino/aliyungo/ram" packerecs "github.com/hashicorp/packer/builder/alicloud/ecs" "github.com/hashicorp/packer/common" "github.com/hashicorp/packer/helper/config" "github.com/hashicorp/packer/packer" "github.com/hashicorp/packer/template/interpolate" ) const ( BuilderId = "packer.post-processor.alicloud-import" OSSSuffix = "oss-" RAWFileFormat = "raw" VHDFileFormat = "vhd" BUSINESSINFO = "packer" AliyunECSImageImportDefaultRolePolicy = `{ "Statement": [ { "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": [ "ecs.aliyuncs.com" ] } } ], "Version": "1" }` ) // Configuration of this post processor type Config struct { common.PackerConfig `mapstructure:",squash"` packerecs.Config `mapstructure:",squash"` // Variables specific to this post processor OSSBucket string `mapstructure:"oss_bucket_name"` OSSKey string `mapstructure:"oss_key_name"` SkipClean bool `mapstructure:"skip_clean"` Tags map[string]string `mapstructure:"tags"` AlicloudImageName string `mapstructure:"image_name"` AlicloudImageVersion string `mapstructure:"image_version"` AlicloudImageDescription string `mapstructure:"image_description"` AlicloudImageShareAccounts []string `mapstructure:"image_share_account"` AlicloudImageDestinationRegions []string `mapstructure:"image_copy_regions"` OSType string `mapstructure:"image_os_type"` Platform string `mapstructure:"image_platform"` Architecture string `mapstructure:"image_architecture"` Size string `mapstructure:"image_system_size"` Format string `mapstructure:"format"` AlicloudImageForceDelete bool `mapstructure:"image_force_delete"` ctx interpolate.Context } type PostProcessor struct { config Config DiskDeviceMapping []ecs.DiskDeviceMapping } // Entry point for configuration parsing when we've defined func (p *PostProcessor) Configure(raws ...interface{}) error { err := config.Decode(&p.config, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &p.config.ctx, InterpolateFilter: &interpolate.RenderFilter{ Exclude: []string{ "oss_key_name", }, }, }, raws...) if err != nil { return err } errs := new(packer.MultiError) // Check and render oss_key_name if err = interpolate.Validate(p.config.OSSKey, &p.config.ctx); err != nil { errs = packer.MultiErrorAppend( errs, fmt.Errorf("Error parsing oss_key_name template: %s", err)) } // Check we have alicloud access variables defined somewhere errs = packer.MultiErrorAppend(errs, p.config.AlicloudAccessConfig.Prepare(&p.config.ctx)...) // define all our required parameters templates := map[string]*string{ "oss_bucket_name": &p.config.OSSBucket, } // Check out required params are defined for key, ptr := range templates { if *ptr == "" { errs = packer.MultiErrorAppend( errs, fmt.Errorf("%s must be set", key)) } } // Anything which flagged return back up the stack if len(errs.Errors) > 0 { return errs } packer.LogSecretFilter.Set(p.config.AlicloudAccessKey, p.config.AlicloudSecretKey) log.Println(p.config) return nil } func (p *PostProcessor) PostProcess(ctx context.Context, ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { var err error // Render this key since we didn't in the configure phase p.config.OSSKey, err = interpolate.Render(p.config.OSSKey, &p.config.ctx) if err != nil { return nil, false, fmt.Errorf("Error rendering oss_key_name template: %s", err) } if p.config.OSSKey == "" { p.config.OSSKey = "Packer_" + strconv.Itoa(time.Now().Nanosecond()) } log.Printf("Rendered oss_key_name as %s", p.config.OSSKey) log.Println("Looking for RAW or VHD in artifact") // Locate the files output from the builder source := "" for _, path := range artifact.Files() { if strings.HasSuffix(path, VHDFileFormat) || strings.HasSuffix(path, RAWFileFormat) { source = path break } } // Hope we found something useful if source == "" { return nil, false, fmt.Errorf("No vhd or raw file found in artifact from builder") } ecsClient, err := p.config.AlicloudAccessConfig.Client() if err != nil { return nil, false, fmt.Errorf("Failed to connect alicloud ecs %s", err) } ecsClient.SetBusinessInfo(BUSINESSINFO) images, _, err := ecsClient.DescribeImages(&ecs.DescribeImagesArgs{ RegionId: packercommon.Region(p.config.AlicloudRegion), ImageName: p.config.AlicloudImageName, }) if err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } if len(images) > 0 && !p.config.AlicloudImageForceDelete { return nil, false, fmt.Errorf("Duplicated image exists, please delete the existing images " + "or set the 'image_force_delete' value as true") } // Set up the OSS client log.Println("Creating OSS Client") client, err := oss.New(getEndPonit(p.config.AlicloudRegion), p.config.AlicloudAccessKey, p.config.AlicloudSecretKey) if err != nil { return nil, false, fmt.Errorf("Creating oss connection failed: %s", err) } bucket, err := queryOrCreateBucket(p.config.OSSBucket, client) if err != nil { return nil, false, fmt.Errorf("Failed to query or create bucket %s: %s", p.config.OSSBucket, err) } if err != nil { return nil, false, fmt.Errorf("Failed to open %s: %s", source, err) } err = bucket.PutObjectFromFile(p.config.OSSKey, source) if err != nil { return nil, false, fmt.Errorf("Failed to upload image %s: %s", source, err) } if len(images) > 0 && p.config.AlicloudImageForceDelete { if err = ecsClient.DeleteImage(packercommon.Region(p.config.AlicloudRegion), images[0].ImageId); err != nil { return nil, false, fmt.Errorf("Delete duplicated image %s failed", images[0].ImageName) } } diskDeviceMapping := ecs.DiskDeviceMapping{ Size: p.config.Size, Format: p.config.Format, OSSBucket: p.config.OSSBucket, OSSObject: p.config.OSSKey, } imageImageArgs := &ecs.ImportImageArgs{ RegionId: packercommon.Region(p.config.AlicloudRegion), ImageName: p.config.AlicloudImageName, ImageVersion: p.config.AlicloudImageVersion, Description: p.config.AlicloudImageDescription, Architecture: p.config.Architecture, OSType: p.config.OSType, Platform: p.config.Platform, } imageImageArgs.DiskDeviceMappings.DiskDeviceMapping = []ecs.DiskDeviceMapping{ diskDeviceMapping, } imageId, err := ecsClient.ImportImage(imageImageArgs) if err != nil { e, _ := err.(*packercommon.Error) if e.Code == "NoSetRoletoECSServiceAccount" { ramClient := ram.NewClient(p.config.AlicloudAccessKey, p.config.AlicloudSecretKey) roleResponse, err := ramClient.GetRole(ram.RoleQueryRequest{ RoleName: "AliyunECSImageImportDefaultRole", }) if err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } if roleResponse.Role.RoleId == "" { if _, err = ramClient.CreateRole(ram.RoleRequest{ RoleName: "AliyunECSImageImportDefaultRole", AssumeRolePolicyDocument: AliyunECSImageImportDefaultRolePolicy, }); err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } if _, err := ramClient.AttachPolicyToRole(ram.AttachPolicyToRoleRequest{ PolicyRequest: ram.PolicyRequest{ PolicyName: "AliyunECSImageImportRolePolicy", PolicyType: "System", }, RoleName: "AliyunECSImageImportDefaultRole", }); err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } } else { policyListResponse, err := ramClient.ListPoliciesForRole(ram.RoleQueryRequest{ RoleName: "AliyunECSImageImportDefaultRole", }) if err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } isAliyunECSImageImportRolePolicyNotExit := true for _, policy := range policyListResponse.Policies.Policy { if policy.PolicyName == "AliyunECSImageImportRolePolicy" && policy.PolicyType == "System" { isAliyunECSImageImportRolePolicyNotExit = false break } } if isAliyunECSImageImportRolePolicyNotExit { if _, err := ramClient.AttachPolicyToRole(ram.AttachPolicyToRoleRequest{ PolicyRequest: ram.PolicyRequest{ PolicyName: "AliyunECSImageImportRolePolicy", PolicyType: "System", }, RoleName: "AliyunECSImageImportDefaultRole", }); err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } } if _, err = ramClient.UpdateRole( ram.UpdateRoleRequest{ RoleName: "AliyunECSImageImportDefaultRole", NewAssumeRolePolicyDocument: AliyunECSImageImportDefaultRolePolicy, }); err != nil { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } } for i := 10; i > 0; i = i - 1 { imageId, err = ecsClient.ImportImage(imageImageArgs) if err != nil { e, _ = err.(*packercommon.Error) if e.Code == "NoSetRoletoECSServiceAccount" { time.Sleep(5 * time.Second) continue } else if e.Code == "ImageIsImporting" || e.Code == "InvalidImageName.Duplicated" { break } return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } break } } else { return nil, false, fmt.Errorf("Failed to start import from %s/%s: %s", getEndPonit(p.config.OSSBucket), p.config.OSSKey, err) } } err = ecsClient.WaitForImageReady(packercommon.Region(p.config.AlicloudRegion), imageId, packerecs.ALICLOUD_DEFAULT_LONG_TIMEOUT) // Add the reported Alicloud image ID to the artifact list log.Printf("Importing created alicloud image ID %s in region %s Finished.", imageId, p.config.AlicloudRegion) artifact = &packerecs.Artifact{ AlicloudImages: map[string]string{ p.config.AlicloudRegion: imageId, }, BuilderIdValue: BuilderId, Client: ecsClient, } if !p.config.SkipClean { ui.Message(fmt.Sprintf("Deleting import source %s/%s/%s", getEndPonit(p.config.AlicloudRegion), p.config.OSSBucket, p.config.OSSKey)) if err = bucket.DeleteObject(p.config.OSSKey); err != nil { return nil, false, fmt.Errorf("Failed to delete %s/%s/%s: %s", getEndPonit(p.config.AlicloudRegion), p.config.OSSBucket, p.config.OSSKey, err) } } return artifact, false, nil } func queryOrCreateBucket(bucketName string, client *oss.Client) (*oss.Bucket, error) { isExist, err := client.IsBucketExist(bucketName) if err != nil { return nil, err } if !isExist { err = client.CreateBucket(bucketName) if err != nil { return nil, err } } bucket, err := client.Bucket(bucketName) if err != nil { return nil, err } return bucket, nil } func getEndPonit(region string) string { return "https://" + GetOSSRegion(region) + ".aliyuncs.com" } func GetOSSRegion(region string) string { if strings.HasPrefix(region, OSSSuffix) { return region } return OSSSuffix + region } func GetECSRegion(region string) string { if strings.HasPrefix(region, OSSSuffix) { return strings.TrimSuffix(region, OSSSuffix) } return region }