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  }