diff --git a/.web-docs/components/post-processor/import/README.md b/.web-docs/components/post-processor/import/README.md index 5ffbeda3..4de8f01d 100644 --- a/.web-docs/components/post-processor/import/README.md +++ b/.web-docs/components/post-processor/import/README.md @@ -94,18 +94,39 @@ Optional: must be set to `uefi`. - `platform` (string) - The operating system of the virtual machine. One of: - `linux` or `windows`. If `boot_mode` is set to `uefi` then this value must be - set to either `windows` or `linux` depending on the operating system of the - virtual machine. + `linux` or `windows`. If `boot_mode` is set to `uefi` then this value must be + set to either `windows` or `linux` depending on the operating system of the + virtual machine. `windows` can only be used here when `import_type` is `image`. - `custom_endpoint_ec2` (string) - This option is useful if you use a cloud provider whose API is compatible with aws EC2. Specify another endpoint like this `https://ec2.custom.endpoint.com`. +- `ena_support` (boolean) - Only applicable if `import_type` is set to + `snapshot`. This sets a flag on the AMI indicating that the image includes + support for the + [Elastic Network Adapter](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking-ena.html). + Defaults to `false`. + - `format` (string) - One of: `ova`, `raw`, `vhd`, `vhdx`, or `vmdk`. This specifies the format of the source virtual machine image. The resulting artifact from the builder is assumed to have a file extension matching the - format. This defaults to `ova`. + format. This defaults to `ova` if `import_type` is `image`, and `raw` if + `import_type` is `snapshot`. + +- `import_type` (string) - The method to use to import the image. + One of: `image` or `snapshot`. If set to `image`, the + [ImportImage](https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html) + API is used to perform the import, which only supports a limited number of + [operating systems](https://docs.aws.amazon.com/vm-import/latest/userguide/prerequisites.html#vmimport-operating-systems) + and performs + [programmatic modifications](https://docs.aws.amazon.com/vm-import/latest/userguide/import-modify-vm.html) + to the image during the import process. If set to `snapshot`, the + [ImportSnapshot](https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-import-snapshot.html) + API is used and then the resulting snapshot is registered as an AMI, which + does not perform any modifications to the image, supports a wider range of + Linux distributions, but does not support importing Windows images. + The default is `image`. - `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of the AWS EC2 endpoint. The default is `false`. @@ -117,7 +138,8 @@ Optional: Machine Image (AMI) after importing. Valid values: `AWS` or `BYOL` (default). For more details regarding licensing, see [Prerequisites](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/VMImportPrerequisites.html) - in the VM Import/Export User Guide. + in the VM Import/Export User Guide. If `import_type` is set to `snapshot`, this + is ignored. - `mfa_code` (string) - The MFA [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) @@ -159,6 +181,10 @@ Optional: - `skip_region_validation` (boolean) - Set to true if you want to skip validation of the region configuration option. Default `false`. +- `snapshot_device_name` (string) - The root device name to use in the block + device mapping when registering a snapshot import as an AMI. Only applicable + if `import_type` is `snapshot`. Defaults to `/dev/sda`. + - `tags` (object of key/value strings) - Tags applied to the created AMI and relevant snapshots. @@ -167,6 +193,11 @@ Optional: probably don't need it. This will also be read from the `AWS_SESSION_TOKEN` environmental variable. +- `virtualization_type` (string) - The virtualization type to be used for + the imported AMI. One of: `hvm` or `paravirtual`. Defaults to `hvm`, + `paravirtual` is only supported on previous-generation EC2 instance types. + This option can only be set when `import_type` is set to `snapshot`. + ## Basic Example Here is a basic example. This assumes that the builder has produced an OVA diff --git a/builder/common/state.go b/builder/common/state.go index 123568bc..5706d184 100644 --- a/builder/common/state.go +++ b/builder/common/state.go @@ -227,6 +227,18 @@ func (w *AWSPollingConfig) WaitUntilFastLaunchEnabled(ctx aws.Context, conn *ec2 return err } +func (w *AWSPollingConfig) WaitUntilSnapshotImported(ctx aws.Context, conn *ec2.EC2, taskID string) error { + importInput := ec2.DescribeImportSnapshotTasksInput{ + ImportTaskIds: []*string{&taskID}, + } + + err := WaitForSnapshotToBeImported(conn, + ctx, + &importInput, + w.getWaiterOptions()...) + return err +} + // Custom waiters using AWS's request.Waiter func WaitForVolumeToBeAttached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error { @@ -371,6 +383,43 @@ func WaitUntilFastLaunchEnabled(c *ec2.EC2, ctx aws.Context, input *ec2.Describe return w.WaitWithContext(ctx) } +func WaitForSnapshotToBeImported(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeImportSnapshotTasksInput, opts ...request.WaiterOption) error { + w := request.Waiter{ + Name: "DescribeSnapshot", + MaxAttempts: 720, + Delay: request.ConstantWaiterDelay(5 * time.Second), + Acceptors: []request.WaiterAcceptor{ + { + State: request.SuccessWaiterState, + Matcher: request.PathAllWaiterMatch, + Argument: "ImportSnapshotTasks[].SnapshotTaskDetail.Status", + Expected: "completed", + }, + { + State: request.FailureWaiterState, + Matcher: request.PathAnyWaiterMatch, + Argument: "ImportSnapshotTasks[].SnapshotTaskDetail.Status", + Expected: "deleted", + }, + }, + Logger: c.Config.Logger, + NewRequest: func(opts []request.Option) (*request.Request, error) { + var inCpy *ec2.DescribeImportSnapshotTasksInput + if input != nil { + tmp := *input + inCpy = &tmp + } + req, _ := c.DescribeImportSnapshotTasksRequest(inCpy) + req.SetContext(ctx) + req.ApplyOptions(opts...) + return req, nil + }, + } + w.ApplyOptions(opts...) + + return w.WaitWithContext(ctx) +} + // This helper function uses the environment variables AWS_TIMEOUT_SECONDS and // AWS_POLL_DELAY_SECONDS to generate waiter options that can be passed into any // request.Waiter function. These options will control how many times the waiter diff --git a/docs/post-processors/import.mdx b/docs/post-processors/import.mdx index 5133e49b..efe98e6f 100644 --- a/docs/post-processors/import.mdx +++ b/docs/post-processors/import.mdx @@ -104,18 +104,39 @@ Optional: must be set to `uefi`. - `platform` (string) - The operating system of the virtual machine. One of: - `linux` or `windows`. If `boot_mode` is set to `uefi` then this value must be - set to either `windows` or `linux` depending on the operating system of the - virtual machine. + `linux` or `windows`. If `boot_mode` is set to `uefi` then this value must be + set to either `windows` or `linux` depending on the operating system of the + virtual machine. `windows` can only be used here when `import_type` is `image`. - `custom_endpoint_ec2` (string) - This option is useful if you use a cloud provider whose API is compatible with aws EC2. Specify another endpoint like this `https://ec2.custom.endpoint.com`. +- `ena_support` (boolean) - Only applicable if `import_type` is set to + `snapshot`. This sets a flag on the AMI indicating that the image includes + support for the + [Elastic Network Adapter](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/enhanced-networking-ena.html). + Defaults to `false`. + - `format` (string) - One of: `ova`, `raw`, `vhd`, `vhdx`, or `vmdk`. This specifies the format of the source virtual machine image. The resulting artifact from the builder is assumed to have a file extension matching the - format. This defaults to `ova`. + format. This defaults to `ova` if `import_type` is `image`, and `raw` if + `import_type` is `snapshot`. + +- `import_type` (string) - The method to use to import the image. + One of: `image` or `snapshot`. If set to `image`, the + [ImportImage](https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-image-import.html) + API is used to perform the import, which only supports a limited number of + [operating systems](https://docs.aws.amazon.com/vm-import/latest/userguide/prerequisites.html#vmimport-operating-systems) + and performs + [programmatic modifications](https://docs.aws.amazon.com/vm-import/latest/userguide/import-modify-vm.html) + to the image during the import process. If set to `snapshot`, the + [ImportSnapshot](https://docs.aws.amazon.com/vm-import/latest/userguide/vmimport-import-snapshot.html) + API is used and then the resulting snapshot is registered as an AMI, which + does not perform any modifications to the image, supports a wider range of + Linux distributions, but does not support importing Windows images. + The default is `image`. - `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of the AWS EC2 endpoint. The default is `false`. @@ -127,7 +148,8 @@ Optional: Machine Image (AMI) after importing. Valid values: `AWS` or `BYOL` (default). For more details regarding licensing, see [Prerequisites](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/VMImportPrerequisites.html) - in the VM Import/Export User Guide. + in the VM Import/Export User Guide. If `import_type` is set to `snapshot`, this + is ignored. - `mfa_code` (string) - The MFA [TOTP](https://en.wikipedia.org/wiki/Time-based_One-time_Password_Algorithm) @@ -169,6 +191,10 @@ Optional: - `skip_region_validation` (boolean) - Set to true if you want to skip validation of the region configuration option. Default `false`. +- `snapshot_device_name` (string) - The root device name to use in the block + device mapping when registering a snapshot import as an AMI. Only applicable + if `import_type` is `snapshot`. Defaults to `/dev/sda`. + - `tags` (object of key/value strings) - Tags applied to the created AMI and relevant snapshots. @@ -177,6 +203,11 @@ Optional: probably don't need it. This will also be read from the `AWS_SESSION_TOKEN` environmental variable. +- `virtualization_type` (string) - The virtualization type to be used for + the imported AMI. One of: `hvm` or `paravirtual`. Defaults to `hvm`, + `paravirtual` is only supported on previous-generation EC2 instance types. + This option can only be set when `import_type` is set to `snapshot`. + ## Basic Example Here is a basic example. This assumes that the builder has produced an OVA diff --git a/post-processor/import/post-processor.go b/post-processor/import/post-processor.go index dc696dfc..465dfe3d 100644 --- a/post-processor/import/post-processor.go +++ b/post-processor/import/post-processor.go @@ -7,6 +7,7 @@ package amazonimport import ( "context" + "errors" "fmt" "log" "os" @@ -34,26 +35,30 @@ type Config struct { awscommon.AccessConfig `mapstructure:",squash"` // Variables specific to this post processor - S3Bucket string `mapstructure:"s3_bucket_name"` - S3Key string `mapstructure:"s3_key_name"` - S3Encryption string `mapstructure:"s3_encryption"` - S3EncryptionKey string `mapstructure:"s3_encryption_key"` - SkipClean bool `mapstructure:"skip_clean"` - Tags map[string]string `mapstructure:"tags"` - Name string `mapstructure:"ami_name"` - Description string `mapstructure:"ami_description"` - Users []string `mapstructure:"ami_users"` - Groups []string `mapstructure:"ami_groups"` - OrgArns []string `mapstructure:"ami_org_arns"` - OuArns []string `mapstructure:"ami_ou_arns"` - Encrypt bool `mapstructure:"ami_encrypt"` - KMSKey string `mapstructure:"ami_kms_key"` - LicenseType string `mapstructure:"license_type"` - RoleName string `mapstructure:"role_name"` - Format string `mapstructure:"format"` - Architecture string `mapstructure:"architecture"` - BootMode string `mapstructure:"boot_mode"` - Platform string `mapstructure:"platform"` + S3Bucket string `mapstructure:"s3_bucket_name"` + S3Key string `mapstructure:"s3_key_name"` + S3Encryption string `mapstructure:"s3_encryption"` + S3EncryptionKey string `mapstructure:"s3_encryption_key"` + SkipClean bool `mapstructure:"skip_clean"` + Tags map[string]string `mapstructure:"tags"` + Name string `mapstructure:"ami_name"` + Description string `mapstructure:"ami_description"` + Users []string `mapstructure:"ami_users"` + Groups []string `mapstructure:"ami_groups"` + OrgArns []string `mapstructure:"ami_org_arns"` + OuArns []string `mapstructure:"ami_ou_arns"` + Encrypt bool `mapstructure:"ami_encrypt"` + KMSKey string `mapstructure:"ami_kms_key"` + LicenseType string `mapstructure:"license_type"` + RoleName string `mapstructure:"role_name"` + Format string `mapstructure:"format"` + Architecture string `mapstructure:"architecture"` + BootMode string `mapstructure:"boot_mode"` + Platform string `mapstructure:"platform"` + ImportType string `mapstructure:"import_type"` + SnapshotDeviceName string `mapstructure:"snapshot_device_name"` + EnaSupport bool `mapstructure:"ena_support"` + VirtualizationType string `mapstructure:"virtualization_type"` ctx interpolate.Context } @@ -81,10 +86,6 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } // Set defaults - if p.config.Format == "" { - p.config.Format = "ova" - } - if p.config.S3Key == "" { p.config.S3Key = "packer-import-{{timestamp}}." + p.config.Format } @@ -93,6 +94,14 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { p.config.Architecture = "x86_64" } + if p.config.ImportType == "" { + p.config.ImportType = "image" + } + + if p.config.SnapshotDeviceName == "" { + p.config.SnapshotDeviceName = "/dev/sda" + } + errs := new(packersdk.MultiError) if p.config.BootMode == "" { @@ -130,24 +139,67 @@ func (p *PostProcessor) Configure(raws ...interface{}) error { } } - switch p.config.Format { - case "ova", "raw", "vmdk", "vhd", "vhdx": - default: - errs = packersdk.MultiErrorAppend( - errs, fmt.Errorf("invalid format '%s'. Only 'ova', 'raw', 'vhd', 'vhdx', or 'vmdk' are allowed", p.config.Format)) - } - - switch p.config.Platform { - case "windows", "linux": - case "": - if p.config.BootMode == "uefi" { + switch p.config.ImportType { + case "image": + switch p.config.Format { + case "ova", "raw", "vmdk", "vhd", "vhdx": + case "": + p.config.Format = "ova" + default: + errs = packersdk.MultiErrorAppend( + errs, fmt.Errorf("invalid format '%s'. Only 'ova', 'raw', 'vhd', 'vhdx', or 'vmdk' are allowed", p.config.Format)) + } + switch p.config.Platform { + case "windows", "linux": + case "": + if p.config.BootMode == "uefi" { + errs = packersdk.MultiErrorAppend( + errs, fmt.Errorf("invalid platform '%s', 'platform' must be set for 'uefi' image imports", p.config.Platform)) + } + default: + errs = packersdk.MultiErrorAppend(errs, fmt.Errorf( + "invalid platform '%s'. Only 'linux' and 'windows' are allowed", p.config.Platform)) + } + if p.config.VirtualizationType != "" { + errs = packersdk.MultiErrorAppend(errs, errors.New( + "virtualization_type can only be specified when import_type='snapshot'")) + } + case "snapshot": + // If importing to snapshot, only 3 formats are allowed + switch p.config.Format { + case "raw", "vhd", "vmdk": + case "": + p.config.Format = "raw" + default: errs = packersdk.MultiErrorAppend( - errs, fmt.Errorf("invalid platform '%s', 'platform' must be set for 'uefi' image imports", p.config.Platform)) + errs, fmt.Errorf( + "invalid format '%s' for snapshot import. Only 'raw', 'vhd', or 'vmdk' are allowed", p.config.Format)) + } + // Platform is not used as an AWS parameter for snapshot imports, AWS assumes 'linux' platform in this case. + switch p.config.Platform { + case "", "linux": + default: + errs = packersdk.MultiErrorAppend( + errs, fmt.Errorf("invalid platform '%s', only 'linux' is allowed when import_type='snapshot'", p.config.Platform)) + } + switch p.config.VirtualizationType { + case "paravirtual": + if p.config.Architecture == "arm64" { + errs = packersdk.MultiErrorAppend( + errs, errors.New("only 'hvm' virtualization_type is allowed for 'arm64' architecture")) + } + case "hvm": + case "": + p.config.VirtualizationType = "hvm" + default: + errs = packersdk.MultiErrorAppend( + errs, fmt.Errorf("invalid virtualization_type '%s', only 'hvm' or 'paravirtual' are allowed", p.config.VirtualizationType)) } default: - errs = packersdk.MultiErrorAppend(errs, fmt.Errorf( - "invalid platform '%s'. Only 'linux' and 'windows' are allowed", p.config.Platform)) + errs = packersdk.MultiErrorAppend( + errs, fmt.Errorf("invalid import_type '%s'. Only 'image' or 'snapshot' are allowed", p.config.ImportType)) } + if p.config.S3Encryption != "" && p.config.S3Encryption != "AES256" && p.config.S3Encryption != "aws:kms" { errs = packersdk.MultiErrorAppend( errs, fmt.Errorf("invalid s3 encryption format '%s'. Only 'AES256' and 'aws:kms' are allowed", p.config.S3Encryption)) @@ -257,93 +309,22 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifa // Call EC2 image import process log.Printf("Calling EC2 to import from s3://%s/%s", p.config.S3Bucket, p.config.S3Key) + // Split into snapshot or image import + var createdami string ec2conn := ec2.New(session) - params := &ec2.ImportImageInput{ - Encrypted: &p.config.Encrypt, - DiskContainers: []*ec2.ImageDiskContainer{ - { - Format: &p.config.Format, - UserBucket: &ec2.UserBucket{ - S3Bucket: &p.config.S3Bucket, - S3Key: &p.config.S3Key, - }, - }, - }, - Architecture: &p.config.Architecture, - BootMode: &p.config.BootMode, - Platform: &p.config.Platform, - } - - if p.config.Encrypt && p.config.KMSKey != "" { - params.KmsKeyId = &p.config.KMSKey - } - - if p.config.RoleName != "" { - params.SetRoleName(p.config.RoleName) - } - - if p.config.LicenseType != "" { - ui.Message(fmt.Sprintf("Setting license type to '%s'", p.config.LicenseType)) - params.LicenseType = &p.config.LicenseType - } - - var import_start *ec2.ImportImageOutput - err = retry.Config{ - Tries: 11, - RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, - }.Run(ctx, func(ctx context.Context) error { - import_start, err = ec2conn.ImportImage(params) - return err - }) - if err != nil { - return nil, false, false, fmt.Errorf("Failed to start import from s3://%s/%s: %s", p.config.S3Bucket, p.config.S3Key, err) - } - - ui.Message(fmt.Sprintf("Started import of s3://%s/%s, task id %s", p.config.S3Bucket, p.config.S3Key, *import_start.ImportTaskId)) - - // Wait for import process to complete, this takes a while - ui.Message(fmt.Sprintf("Waiting for task %s to complete (may take a while)", *import_start.ImportTaskId)) - err = p.config.PollingConfig.WaitUntilImageImported(aws.BackgroundContext(), ec2conn, *import_start.ImportTaskId) - if err != nil { - - // Retrieve the status message - import_result, err2 := ec2conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{ - ImportTaskIds: []*string{ - import_start.ImportTaskId, - }, - }) - - statusMessage := "Error retrieving status message" - - if err2 == nil { - statusMessage = *import_result.ImportImageTasks[0].StatusMessage - } - return nil, false, false, fmt.Errorf("Import task %s failed with status message: %s, error: %s", *import_start.ImportTaskId, statusMessage, err) + if p.config.ImportType == "snapshot" { + createdami, err = p.importSnapshot(ui, ctx, ec2conn) + } else { + createdami, err = p.importImage(ui, ctx, ec2conn) } - // Retrieve what the outcome was for the import task - import_result, err := ec2conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{ - ImportTaskIds: []*string{ - import_start.ImportTaskId, - }, - }) - if err != nil { - return nil, false, false, fmt.Errorf("Failed to find import task %s: %s", *import_start.ImportTaskId, err) - } - // Check it was actually completed - if *import_result.ImportImageTasks[0].Status != "completed" { - // The most useful error message is from the job itself - return nil, false, false, fmt.Errorf("Import task %s failed: %s", *import_start.ImportTaskId, *import_result.ImportImageTasks[0].StatusMessage) + return nil, false, false, err } - ui.Message(fmt.Sprintf("Import task %s complete", *import_start.ImportTaskId)) - - // Pull AMI ID out of the completed job - createdami := *import_result.ImportImageTasks[0].ImageId - - if p.config.Name != "" { + // Dont rename on snapshot as we set the name on creation + if p.config.Name != "" && p.config.ImportType != "snapshot" { ui.Message(fmt.Sprintf("Starting rename of AMI (%s)", createdami)) @@ -546,3 +527,212 @@ func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifa return artifact, false, false, nil } + +func (p *PostProcessor) importSnapshot(ui packersdk.Ui, ctx context.Context, ec2conn *ec2.EC2) (string, error) { + var err error + var importResult *ec2.DescribeImportSnapshotTasksOutput + + params := &ec2.ImportSnapshotInput{ + Encrypted: &p.config.Encrypt, + DiskContainer: &ec2.SnapshotDiskContainer{ + Format: &p.config.Format, + UserBucket: &ec2.UserBucket{ + S3Bucket: &p.config.S3Bucket, + S3Key: &p.config.S3Key, + }, + }, + } + + if p.config.Encrypt && p.config.KMSKey != "" { + params.KmsKeyId = &p.config.KMSKey + } + + if p.config.RoleName != "" { + params.SetRoleName(p.config.RoleName) + } + + var importStart *ec2.ImportSnapshotOutput + err = retry.Config{ + Tries: 11, + RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, + }.Run(ctx, func(ctx context.Context) error { + importStart, err = ec2conn.ImportSnapshot(params) + return err + }) + + if err != nil { + return "", fmt.Errorf("Failed to start snapshot import from s3://%s/%s: %s", p.config.S3Bucket, p.config.S3Key, err) + } + + importTaskId := importStart.ImportTaskId + + ui.Message(fmt.Sprintf("Started snapshot import of s3://%s/%s, task id %s", p.config.S3Bucket, p.config.S3Key, *importTaskId)) + + // Wait for import process to complete, this takes a while + ui.Message(fmt.Sprintf("Waiting for import snapshot task %s to complete (may take a while)", *importTaskId)) + + err = p.config.PollingConfig.WaitUntilSnapshotImported(aws.BackgroundContext(), ec2conn, *importTaskId) + if err != nil { + // Retrieve the status message + if importResult, describeErr := ec2conn.DescribeImportSnapshotTasks(&ec2.DescribeImportSnapshotTasksInput{ + ImportTaskIds: []*string{ + importTaskId, + }, + }); describeErr != nil { + return "", fmt.Errorf("Import snapshot task %s failed with status message: %s, error: %s", *importTaskId, *importResult.ImportSnapshotTasks[0].SnapshotTaskDetail.StatusMessage, err) + } + + return "", fmt.Errorf("Import snapshot task %s failed with status message: Error retrieving status message, error: %s", *importTaskId, err) + } + + // Retrieve what the outcome was for the import task + importResult, err = ec2conn.DescribeImportSnapshotTasks(&ec2.DescribeImportSnapshotTasksInput{ + ImportTaskIds: []*string{ + importTaskId, + }, + }) + + if err != nil { + return "", fmt.Errorf("Failed to find import snapshot task %s: %s", *importTaskId, err) + } + + snapshotId := importResult.ImportSnapshotTasks[0].SnapshotTaskDetail.SnapshotId + + // Check it was actually completed + if *importResult.ImportSnapshotTasks[0].SnapshotTaskDetail.Status != "completed" { + // The most useful error message is from the job itself + return "", fmt.Errorf("Import snapshot task %s failed: %s", *importTaskId, *importResult.ImportSnapshotTasks[0].SnapshotTaskDetail.StatusMessage) + } + + ui.Message(fmt.Sprintf("Import snapshot task %s complete", *importTaskId)) + + ebsDevice := ec2.EbsBlockDevice{ + SnapshotId: snapshotId, + } + + blockDevice := ec2.BlockDeviceMapping{ + DeviceName: &p.config.SnapshotDeviceName, + Ebs: &ebsDevice, + } + + var imageName string + + // Unfortunately when importing from snapshot we need to give a name to the AMI, + // so either get the name from the config or set a name based on the source s3 object. + if p.config.Name != "" { + imageName = p.config.Name + } else { + imageName = fmt.Sprintf("packer-import-from-%s", p.config.S3Key) + } + + registerImageOutput, err := ec2conn.RegisterImage(&ec2.RegisterImageInput{ + BlockDeviceMappings: []*ec2.BlockDeviceMapping{ + &blockDevice, + }, + RootDeviceName: &p.config.SnapshotDeviceName, + Name: &imageName, + Architecture: &p.config.Architecture, + BootMode: &p.config.BootMode, + EnaSupport: &p.config.EnaSupport, + VirtualizationType: &p.config.VirtualizationType, + }) + + if err != nil { + return "", fmt.Errorf("Failed to register snapshot %s as AMI: %s", *snapshotId, err) + } + + // Pull AMI ID out of the completed job + createdAmi := *registerImageOutput.ImageId + return createdAmi, err +} + +func (p *PostProcessor) importImage(ui packersdk.Ui, ctx context.Context, ec2conn *ec2.EC2) (string, error) { + var err error + var value string + params := &ec2.ImportImageInput{ + Encrypted: &p.config.Encrypt, + DiskContainers: []*ec2.ImageDiskContainer{ + { + Format: &p.config.Format, + UserBucket: &ec2.UserBucket{ + S3Bucket: &p.config.S3Bucket, + S3Key: &p.config.S3Key, + }, + }, + }, + Architecture: &p.config.Architecture, + BootMode: &p.config.BootMode, + Platform: &p.config.Platform, + } + + if p.config.Encrypt && p.config.KMSKey != "" { + params.KmsKeyId = &p.config.KMSKey + } + + if p.config.RoleName != "" { + params.SetRoleName(p.config.RoleName) + } + + if p.config.LicenseType != "" { + ui.Message(fmt.Sprintf("Setting license type to '%s'", p.config.LicenseType)) + params.LicenseType = &p.config.LicenseType + } + + var importStart *ec2.ImportImageOutput + err = retry.Config{ + Tries: 11, + RetryDelay: (&retry.Backoff{InitialBackoff: 200 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear, + }.Run(ctx, func(ctx context.Context) error { + importStart, err = ec2conn.ImportImage(params) + return err + }) + + if err != nil { + return value, fmt.Errorf("Failed to start image import from s3://%s/%s: %s", p.config.S3Bucket, p.config.S3Key, err) + } + + importTaskId := importStart.ImportTaskId + + ui.Message(fmt.Sprintf("Started image import of s3://%s/%s, task id %s", p.config.S3Bucket, p.config.S3Key, *importTaskId)) + + // Wait for import process to complete, this takes a while + ui.Message(fmt.Sprintf("Waiting for import image task %s to complete (may take a while)", *importTaskId)) + err = p.config.PollingConfig.WaitUntilImageImported(aws.BackgroundContext(), ec2conn, *importTaskId) + if err != nil { + // Retrieve the status message + importResult, err2 := ec2conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{ + ImportTaskIds: []*string{ + importTaskId, + }, + }) + + statusMessage := "Error retrieving status message" + + if err2 == nil { + statusMessage = *importResult.ImportImageTasks[0].StatusMessage + } + return value, fmt.Errorf("Import image task %s failed with status message: %s, error: %s", *importTaskId, statusMessage, err) + } + + // Retrieve what the outcome was for the import task + importResult, err := ec2conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{ + ImportTaskIds: []*string{ + importTaskId, + }, + }) + + if err != nil { + return value, fmt.Errorf("Failed to find import image task %s: %s", *importTaskId, err) + } + // Check it was actually completed + if *importResult.ImportImageTasks[0].Status != "completed" { + // The most useful error message is from the job itself + return value, fmt.Errorf("Import image task %s failed: %s", *importTaskId, *importResult.ImportImageTasks[0].StatusMessage) + } + + ui.Message(fmt.Sprintf("Import image task %s complete", *importTaskId)) + + // Pull AMI ID out of the completed job + createdAmi := *importResult.ImportImageTasks[0].ImageId + return createdAmi, err +} diff --git a/post-processor/import/post-processor.hcl2spec.go b/post-processor/import/post-processor.hcl2spec.go index f7e11d82..a1a14d86 100644 --- a/post-processor/import/post-processor.hcl2spec.go +++ b/post-processor/import/post-processor.hcl2spec.go @@ -55,6 +55,10 @@ type FlatConfig struct { Architecture *string `mapstructure:"architecture" cty:"architecture" hcl:"architecture"` BootMode *string `mapstructure:"boot_mode" cty:"boot_mode" hcl:"boot_mode"` Platform *string `mapstructure:"platform" cty:"platform" hcl:"platform"` + ImportType *string `mapstructure:"import_type" cty:"import_type" hcl:"import_type"` + SnapshotDeviceName *string `mapstructure:"snapshot_device_name" cty:"snapshot_device_name" hcl:"snapshot_device_name"` + EnaSupport *bool `mapstructure:"ena_support" cty:"ena_support" hcl:"ena_support"` + VirtualizationType *string `mapstructure:"virtualization_type" cty:"virtualization_type" hcl:"virtualization_type"` } // FlatMapstructure returns a new FlatConfig. @@ -113,6 +117,10 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "architecture": &hcldec.AttrSpec{Name: "architecture", Type: cty.String, Required: false}, "boot_mode": &hcldec.AttrSpec{Name: "boot_mode", Type: cty.String, Required: false}, "platform": &hcldec.AttrSpec{Name: "platform", Type: cty.String, Required: false}, + "import_type": &hcldec.AttrSpec{Name: "import_type", Type: cty.String, Required: false}, + "snapshot_device_name": &hcldec.AttrSpec{Name: "snapshot_device_name", Type: cty.String, Required: false}, + "ena_support": &hcldec.AttrSpec{Name: "ena_support", Type: cty.Bool, Required: false}, + "virtualization_type": &hcldec.AttrSpec{Name: "virtualization_type", Type: cty.String, Required: false}, } return s }