From 1f7648436681e81e5dad781c2498600121cb35b2 Mon Sep 17 00:00:00 2001 From: Itxaka Date: Fri, 2 Jul 2021 17:42:01 +0200 Subject: [PATCH] Add snapshot import to amazon-import This patch adds the possibility of importing an image to Amazon EC2 by using the ImportSnapshot/RegisterImage API, which has lower requirements than the ImportImage API and does not try to modify the imported image. It reuses the current post-process method but diverges once we need to import the image. The artifact upload to S3 is the same, but instead of calling ImportImage, we call ImportSnapshot to create an EBS snapshot from the S3 artifact, then call RegisterImage to register the snapshot as a new AMI. The steps after registering the AMI are identical to the previous image import process. Signed-off-by: Itxaka Co-authored-by: Hugh Cole-Baker Signed-off-by: Hugh Cole-Baker --- .../post-processor/import/README.md | 41 +- builder/common/state.go | 49 ++ docs/post-processors/import.mdx | 41 +- post-processor/import/post-processor.go | 426 +++++++++++++----- .../import/post-processor.hcl2spec.go | 8 + 5 files changed, 437 insertions(+), 128 deletions(-) diff --git a/.web-docs/components/post-processor/import/README.md b/.web-docs/components/post-processor/import/README.md index 5ffbeda38..4de8f01d2 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 123568bc9..5706d1846 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 5133e49ba..efe98e6f7 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 dc696dfcc..465dfe3db 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 f7e11d82b..a1a14d866 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 }