github.com/mmcquillan/packer@v1.1.1-0.20171009221028-c85cf0483a5d/post-processor/amazon-import/post-processor.go (about) 1 package amazonimport 2 3 import ( 4 "fmt" 5 "log" 6 "os" 7 "strings" 8 9 "github.com/aws/aws-sdk-go/aws" 10 "github.com/aws/aws-sdk-go/service/ec2" 11 "github.com/aws/aws-sdk-go/service/s3" 12 "github.com/aws/aws-sdk-go/service/s3/s3manager" 13 awscommon "github.com/hashicorp/packer/builder/amazon/common" 14 "github.com/hashicorp/packer/common" 15 "github.com/hashicorp/packer/helper/config" 16 "github.com/hashicorp/packer/packer" 17 "github.com/hashicorp/packer/template/interpolate" 18 ) 19 20 const BuilderId = "packer.post-processor.amazon-import" 21 22 // Configuration of this post processor 23 type Config struct { 24 common.PackerConfig `mapstructure:",squash"` 25 awscommon.AccessConfig `mapstructure:",squash"` 26 27 // Variables specific to this post processor 28 S3Bucket string `mapstructure:"s3_bucket_name"` 29 S3Key string `mapstructure:"s3_key_name"` 30 SkipClean bool `mapstructure:"skip_clean"` 31 Tags map[string]string `mapstructure:"tags"` 32 Name string `mapstructure:"ami_name"` 33 Description string `mapstructure:"ami_description"` 34 Users []string `mapstructure:"ami_users"` 35 Groups []string `mapstructure:"ami_groups"` 36 LicenseType string `mapstructure:"license_type"` 37 38 ctx interpolate.Context 39 } 40 41 type PostProcessor struct { 42 config Config 43 } 44 45 // Entry point for configuration parsing when we've defined 46 func (p *PostProcessor) Configure(raws ...interface{}) error { 47 p.config.ctx.Funcs = awscommon.TemplateFuncs 48 err := config.Decode(&p.config, &config.DecodeOpts{ 49 Interpolate: true, 50 InterpolateContext: &p.config.ctx, 51 InterpolateFilter: &interpolate.RenderFilter{ 52 Exclude: []string{ 53 "s3_key_name", 54 }, 55 }, 56 }, raws...) 57 if err != nil { 58 return err 59 } 60 61 // Set defaults 62 if p.config.S3Key == "" { 63 p.config.S3Key = "packer-import-{{timestamp}}.ova" 64 } 65 66 errs := new(packer.MultiError) 67 68 // Check and render s3_key_name 69 if err = interpolate.Validate(p.config.S3Key, &p.config.ctx); err != nil { 70 errs = packer.MultiErrorAppend( 71 errs, fmt.Errorf("Error parsing s3_key_name template: %s", err)) 72 } 73 74 // Check we have AWS access variables defined somewhere 75 errs = packer.MultiErrorAppend(errs, p.config.AccessConfig.Prepare(&p.config.ctx)...) 76 77 // define all our required parameters 78 templates := map[string]*string{ 79 "s3_bucket_name": &p.config.S3Bucket, 80 } 81 // Check out required params are defined 82 for key, ptr := range templates { 83 if *ptr == "" { 84 errs = packer.MultiErrorAppend( 85 errs, fmt.Errorf("%s must be set", key)) 86 } 87 } 88 89 // Anything which flagged return back up the stack 90 if len(errs.Errors) > 0 { 91 return errs 92 } 93 94 log.Println(common.ScrubConfig(p.config, p.config.AccessKey, p.config.SecretKey)) 95 return nil 96 } 97 98 func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) { 99 var err error 100 101 session, err := p.config.Session() 102 if err != nil { 103 return nil, false, err 104 } 105 config := session.Config 106 107 // Render this key since we didn't in the configure phase 108 p.config.S3Key, err = interpolate.Render(p.config.S3Key, &p.config.ctx) 109 if err != nil { 110 return nil, false, fmt.Errorf("Error rendering s3_key_name template: %s", err) 111 } 112 log.Printf("Rendered s3_key_name as %s", p.config.S3Key) 113 114 log.Println("Looking for OVA in artifact") 115 // Locate the files output from the builder 116 source := "" 117 for _, path := range artifact.Files() { 118 if strings.HasSuffix(path, ".ova") { 119 source = path 120 break 121 } 122 } 123 124 // Hope we found something useful 125 if source == "" { 126 return nil, false, fmt.Errorf("No OVA file found in artifact from builder") 127 } 128 129 // open the source file 130 log.Printf("Opening file %s to upload", source) 131 file, err := os.Open(source) 132 if err != nil { 133 return nil, false, fmt.Errorf("Failed to open %s: %s", source, err) 134 } 135 136 ui.Message(fmt.Sprintf("Uploading %s to s3://%s/%s", source, p.config.S3Bucket, p.config.S3Key)) 137 138 // Copy the OVA file into the S3 bucket specified 139 uploader := s3manager.NewUploader(session) 140 _, err = uploader.Upload(&s3manager.UploadInput{ 141 Body: file, 142 Bucket: &p.config.S3Bucket, 143 Key: &p.config.S3Key, 144 }) 145 if err != nil { 146 return nil, false, fmt.Errorf("Failed to upload %s: %s", source, err) 147 } 148 149 // May as well stop holding this open now 150 file.Close() 151 152 ui.Message(fmt.Sprintf("Completed upload of %s to s3://%s/%s", source, p.config.S3Bucket, p.config.S3Key)) 153 154 // Call EC2 image import process 155 log.Printf("Calling EC2 to import from s3://%s/%s", p.config.S3Bucket, p.config.S3Key) 156 157 ec2conn := ec2.New(session) 158 params := &ec2.ImportImageInput{ 159 DiskContainers: []*ec2.ImageDiskContainer{ 160 { 161 UserBucket: &ec2.UserBucket{ 162 S3Bucket: &p.config.S3Bucket, 163 S3Key: &p.config.S3Key, 164 }, 165 }, 166 }, 167 } 168 169 if p.config.LicenseType != "" { 170 ui.Message(fmt.Sprintf("Setting license type to '%s'", p.config.LicenseType)) 171 params.LicenseType = &p.config.LicenseType 172 } 173 174 import_start, err := ec2conn.ImportImage(params) 175 176 if err != nil { 177 return nil, false, fmt.Errorf("Failed to start import from s3://%s/%s: %s", p.config.S3Bucket, p.config.S3Key, err) 178 } 179 180 ui.Message(fmt.Sprintf("Started import of s3://%s/%s, task id %s", p.config.S3Bucket, p.config.S3Key, *import_start.ImportTaskId)) 181 182 // Wait for import process to complete, this takes a while 183 ui.Message(fmt.Sprintf("Waiting for task %s to complete (may take a while)", *import_start.ImportTaskId)) 184 185 stateChange := awscommon.StateChangeConf{ 186 Pending: []string{"pending", "active"}, 187 Refresh: awscommon.ImportImageRefreshFunc(ec2conn, *import_start.ImportTaskId), 188 Target: "completed", 189 } 190 191 // Actually do the wait for state change 192 // We ignore errors out of this and check job state in AWS API 193 awscommon.WaitForState(&stateChange) 194 195 // Retrieve what the outcome was for the import task 196 import_result, err := ec2conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{ 197 ImportTaskIds: []*string{ 198 import_start.ImportTaskId, 199 }, 200 }) 201 202 if err != nil { 203 return nil, false, fmt.Errorf("Failed to find import task %s: %s", *import_start.ImportTaskId, err) 204 } 205 206 // Check it was actually completed 207 if *import_result.ImportImageTasks[0].Status != "completed" { 208 // The most useful error message is from the job itself 209 return nil, false, fmt.Errorf("Import task %s failed: %s", *import_start.ImportTaskId, *import_result.ImportImageTasks[0].StatusMessage) 210 } 211 212 ui.Message(fmt.Sprintf("Import task %s complete", *import_start.ImportTaskId)) 213 214 // Pull AMI ID out of the completed job 215 createdami := *import_result.ImportImageTasks[0].ImageId 216 217 if p.config.Name != "" { 218 219 ui.Message(fmt.Sprintf("Starting rename of AMI (%s)", createdami)) 220 221 resp, err := ec2conn.CopyImage(&ec2.CopyImageInput{ 222 Name: &p.config.Name, 223 SourceImageId: &createdami, 224 SourceRegion: config.Region, 225 }) 226 227 if err != nil { 228 return nil, false, fmt.Errorf("Error Copying AMI (%s): %s", createdami, err) 229 } 230 231 ui.Message(fmt.Sprintf("Waiting for AMI rename to complete (may take a while)")) 232 233 stateChange := awscommon.StateChangeConf{ 234 Pending: []string{"pending"}, 235 Target: "available", 236 Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *resp.ImageId), 237 } 238 239 if _, err := awscommon.WaitForState(&stateChange); err != nil { 240 return nil, false, fmt.Errorf("Error waiting for AMI (%s): %s", *resp.ImageId, err) 241 } 242 243 _, err = ec2conn.DeregisterImage(&ec2.DeregisterImageInput{ 244 ImageId: &createdami, 245 }) 246 247 if err != nil { 248 return nil, false, fmt.Errorf("Error deregistering existing AMI: %s", err) 249 } 250 251 ui.Message(fmt.Sprintf("AMI rename completed")) 252 253 createdami = *resp.ImageId 254 } 255 256 // If we have tags, then apply them now to both the AMI and snaps 257 // created by the import 258 if len(p.config.Tags) > 0 { 259 var ec2Tags []*ec2.Tag 260 261 log.Printf("Repacking tags into AWS format") 262 263 for key, value := range p.config.Tags { 264 ui.Message(fmt.Sprintf("Adding tag \"%s\": \"%s\"", key, value)) 265 ec2Tags = append(ec2Tags, &ec2.Tag{ 266 Key: aws.String(key), 267 Value: aws.String(value), 268 }) 269 } 270 271 resourceIds := []*string{&createdami} 272 273 log.Printf("Getting details of %s", createdami) 274 275 imageResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ 276 ImageIds: resourceIds, 277 }) 278 279 if err != nil { 280 return nil, false, fmt.Errorf("Failed to retrieve details for AMI %s: %s", createdami, err) 281 } 282 283 if len(imageResp.Images) == 0 { 284 return nil, false, fmt.Errorf("AMI %s has no images", createdami) 285 } 286 287 image := imageResp.Images[0] 288 289 log.Printf("Walking block device mappings for %s to find snapshots", createdami) 290 291 for _, device := range image.BlockDeviceMappings { 292 if device.Ebs != nil && device.Ebs.SnapshotId != nil { 293 ui.Message(fmt.Sprintf("Tagging snapshot %s", *device.Ebs.SnapshotId)) 294 resourceIds = append(resourceIds, device.Ebs.SnapshotId) 295 } 296 } 297 298 ui.Message(fmt.Sprintf("Tagging AMI %s", createdami)) 299 300 _, err = ec2conn.CreateTags(&ec2.CreateTagsInput{ 301 Resources: resourceIds, 302 Tags: ec2Tags, 303 }) 304 305 if err != nil { 306 return nil, false, fmt.Errorf("Failed to add tags to resources %#v: %s", resourceIds, err) 307 } 308 309 } 310 311 // Apply attributes for AMI specified in config 312 // (duped from builder/amazon/common/step_modify_ami_attributes.go) 313 options := make(map[string]*ec2.ModifyImageAttributeInput) 314 if p.config.Description != "" { 315 options["description"] = &ec2.ModifyImageAttributeInput{ 316 Description: &ec2.AttributeValue{Value: &p.config.Description}, 317 } 318 } 319 320 if len(p.config.Groups) > 0 { 321 groups := make([]*string, len(p.config.Groups)) 322 adds := make([]*ec2.LaunchPermission, len(p.config.Groups)) 323 addGroups := &ec2.ModifyImageAttributeInput{ 324 LaunchPermission: &ec2.LaunchPermissionModifications{}, 325 } 326 327 for i, g := range p.config.Groups { 328 groups[i] = aws.String(g) 329 adds[i] = &ec2.LaunchPermission{ 330 Group: aws.String(g), 331 } 332 } 333 addGroups.UserGroups = groups 334 addGroups.LaunchPermission.Add = adds 335 336 options["groups"] = addGroups 337 } 338 339 if len(p.config.Users) > 0 { 340 users := make([]*string, len(p.config.Users)) 341 adds := make([]*ec2.LaunchPermission, len(p.config.Users)) 342 for i, u := range p.config.Users { 343 users[i] = aws.String(u) 344 adds[i] = &ec2.LaunchPermission{UserId: aws.String(u)} 345 } 346 options["users"] = &ec2.ModifyImageAttributeInput{ 347 UserIds: users, 348 LaunchPermission: &ec2.LaunchPermissionModifications{ 349 Add: adds, 350 }, 351 } 352 } 353 354 if len(options) > 0 { 355 for name, input := range options { 356 ui.Message(fmt.Sprintf("Modifying: %s", name)) 357 input.ImageId = &createdami 358 _, err := ec2conn.ModifyImageAttribute(input) 359 if err != nil { 360 return nil, false, fmt.Errorf("Error modifying AMI attributes: %s", err) 361 } 362 } 363 } 364 365 // Add the reported AMI ID to the artifact list 366 log.Printf("Adding created AMI ID %s in region %s to output artifacts", createdami, *config.Region) 367 artifact = &awscommon.Artifact{ 368 Amis: map[string]string{ 369 *config.Region: createdami, 370 }, 371 BuilderIdValue: BuilderId, 372 Conn: ec2conn, 373 } 374 375 if !p.config.SkipClean { 376 ui.Message(fmt.Sprintf("Deleting import source s3://%s/%s", p.config.S3Bucket, p.config.S3Key)) 377 s3conn := s3.New(session) 378 _, err = s3conn.DeleteObject(&s3.DeleteObjectInput{ 379 Bucket: &p.config.S3Bucket, 380 Key: &p.config.S3Key, 381 }) 382 if err != nil { 383 return nil, false, fmt.Errorf("Failed to delete s3://%s/%s: %s", p.config.S3Bucket, p.config.S3Key, err) 384 } 385 } 386 387 return artifact, false, nil 388 }