Compare commits

..

4 Commits

Author SHA1 Message Date
Wilken Rivera d597e93e70 improve error message 2020-09-25 12:12:31 -04:00
Wilken Rivera 1bb154de5a Add support for reading configs
This is the first step in attempting to read a Packer configuration file
via the packer init command. At the present moment it will try to read
one or more configuration templates from a given directory or path. Once
parsed it will error if parsing fails or exist successfully if it is
able to parse the file.

Looking at how the code is structured there will need to be changes made
to the following places:

- When no configuration file is found Packer will display an error. That
error should be bubbled up a bit so that the caller command can
determine if it should be displayed or not. For packer init no
configuration is not an error. Maybe it should be?

- After a configuration has been parsed there needs to be a single way
to determine a list of plugins associated with the configuration. HCL
and JSON configs have fields for this data but some is exported and some
is unexported. Adapting the packerHandler interface may be an option
here. More investigation needed.
2020-09-16 14:46:16 -04:00
Wilken Rivera 8069ae4f43 Add flagset to init command 2020-09-15 10:19:56 -04:00
Wilken Rivera e8f7076416 Add simple init command 2020-09-14 16:33:51 -04:00
260 changed files with 5246 additions and 26623 deletions
+2 -3
View File
@@ -13,6 +13,5 @@ coverage:
project: off
patch: off
ignore: # ignore hcl2spec generated code for coverage and mocks
- "**/*.hcl2spec.go"
- "**/*_mock.go"
ignore: # ignore hcl2spec generated code for coverage
- "**/*.hcl2spec.go"
+7 -54
View File
@@ -1,85 +1,38 @@
## 1.6.3 (September 25, 2020)
## 1.6.3 (Upcoming)
### IMPROVEMENTS:
* builder/amazon: Add `pause_before_ssm` option to pause for some time before
establishing a Session Manager session; defaults to 10s. [GH-9988]
* builder/amazon: Implement assume_role option that matches Terraform behavior.
[GH-9981]
* builder/azure: Support publishing to a Shared Image Gallery with a different
subscription id [GH-9875]
* builder/openstack: Add `external_source_image_url` and
`external_source_image_format` to support building images from external
source URLs. [GH-9992]
* builder/openstack: Include API requests and responses as part of the debug
log output. [GH-9972]
* builder/oracle-oci: Add `create_vnic_details` option for launch details.
[GH-9856]
* builder/oracle-oci: Allow freeform and defined tags to be added to an instance.
* builder/oracle-oci: Allow freeform and defined tags to be added instance.
[GH-9802]
* builder/proxmox: Add `io_thread` option for supporting io threads when using
a `virtio-scsi-single` controller with a `scsi` or `virtio` disk type.
[GH-9969]
* builder/proxmox: Add ability to specify interfaces for http_directroy and VM.
[GH-9874]
* builder/proxmox: Allow the mounting of multiple ISOs via the `cd_drive`
option. [GH-9653]
* builder/proxmox: Fix boot command special keys. [GH-9885]
* builder/qemu: Add `qemu_img_args` option to set special cli flags for calls
to qemu-img [GH-9956]
* builder/qemu: Add `skip_resize_disk` option to skip the resizing of QCOW2
images. [GH-9896] [GH-9860]
* builder/qemu: Skip qemu-img convert on MacOS to prevent the creation of
corrupt images [QEMU
#1776920](https://bugs.launchpad.net/qemu/+bug/1776920) [GH-9949]
* builder/qemu: New `skip_resize_disk` option. [GH-9896]
* builder/scaleway: Change default boottype to local. [GH-9853]
* builder/scaleway: Update scaleway to use non-deprecated sdk. [GH-9902]
* builder/vmware: Add `vnc_over_websocket` to allow the sending of a
`boot_command` to hosts running ESXi 6.7 and above. [GH-9938]
* builder/vmware: Allow user to set vmware tools source path. [GH-9983]
* builder/vsphere-clone: Add ability to set `mac_address` [GH-9930]
* builder/vsphere-clone: Add floppy_files, cd_files, and iso_paths options.
[GH-9963]
* builder/vsphere-iso: Add NVMe controller support. [GH-9880]
* builder/vsphere: Look for a default resource pool when root resource pool is
not found. [GH-9809]
* core: Add support for running cygwin/msys2 based cd/iso creation tool
[GH-9954]
* core: New `cd_files` option to mount iso for modern OSes which don't support
floppies. [GH-9796] [GH-9919] [GH-9928] [GH-9932] [GH-9941]
floppies. [GH-9796] [GH-9919]
* HCL2: When the type of a variable is not known evaluate setting as a literal
string instead of a variable name. [GH-9863]
* post-processor/vagrant: Support the use of template variables within
Vagrantfile templates. [GH-9923]
* post-processor/yandex-import: Allow custom API endpoint. [GH-9850]
* provisioner/ansible: Add support for Ansible Galaxy Collections. [GH-9903]
### BUG FIXES:
* builder/amazon-ebs: Fix issue where retrying on invalid IAM instance profile
error was creating multiple spot instances. [GH-9946]
* builder/amazon-ebssurrogate: Fix issue where builder defaults to AWS managed
key even when custom `kms_key_id` is set. [GH-9959]
* builder/amazon: Update ssm_driver log polling logic to prevent infinite loops
when SSM driver is terminated outside of Packer. [GH-9991]
* builder/azure: Fix crash when using HCL2 configs. [GH-9984] [GH-9985]
* builder/qemu: Fix hardcoded lowerbound causing negative ports [GH-9905]
* builder/qemu: Skip compaction when backing file is used. [GH-9918]
* builder/scaleway: Add pre validate step to prevent the creation of multiple
images with the same name. [GH-9840]
* builder/vmware-iso: Prevent the use of reserved SCSI ID 0:7 when attaching
multiple disks. [GH-9940]
* builder/vsphere: Fix overly strict iso_path validation regex. [GH-9855]
* command/console: Prevent failure when there are unknown vars. [GH-9864]
* command/inspect: Allow unset variables in HCL2 and JSON. [GH-9832]
* core: Prevent the UI progressbar from hanging and crashing when there is no
TTY available. [GH-9974]
* core: Use $APPDATA over $HOME on Windows hosts when determining homedir.
* core: use $APPDATA over $HOME on Windows hosts when determining homedir.
[GH-9830]
* post-processor/digitalocean-import: Fix crash caused by empty artifact.Files
slice. [GH-9857]
* post-processor/yandex-export: Check for error after runner completes.
[GH-9925]
* post-processor/yandex-export: Set metadata key to expected value on error.
[GH-9849]
* post-processor/yandex-export: Check for error after runner completes.
[GH-9925]
* post-processor/yandex-import: Fix S3 URL construct process. [GH-9931]
## 1.6.2 (August 28, 2020)
@@ -41,9 +41,7 @@ type FlatConfig struct {
SnapshotUsers []string `mapstructure:"snapshot_users" required:"false" cty:"snapshot_users" hcl:"snapshot_users"`
SnapshotGroups []string `mapstructure:"snapshot_groups" required:"false" cty:"snapshot_groups" hcl:"snapshot_groups"`
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
AssumeRole *common.FlatAssumeRoleConfig `mapstructure:"assume_role" required:"false" cty:"assume_role" hcl:"assume_role"`
CustomEndpointEc2 *string `mapstructure:"custom_endpoint_ec2" required:"false" cty:"custom_endpoint_ec2" hcl:"custom_endpoint_ec2"`
CredsFilename *string `mapstructure:"shared_credentials_file" required:"false" cty:"shared_credentials_file" hcl:"shared_credentials_file"`
DecodeAuthZMessages *bool `mapstructure:"decode_authorization_messages" required:"false" cty:"decode_authorization_messages" hcl:"decode_authorization_messages"`
InsecureSkipTLSVerify *bool `mapstructure:"insecure_skip_tls_verify" required:"false" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
MaxRetries *int `mapstructure:"max_retries" required:"false" cty:"max_retries" hcl:"max_retries"`
@@ -119,9 +117,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"snapshot_users": &hcldec.AttrSpec{Name: "snapshot_users", Type: cty.List(cty.String), Required: false},
"snapshot_groups": &hcldec.AttrSpec{Name: "snapshot_groups", Type: cty.List(cty.String), Required: false},
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
"assume_role": &hcldec.BlockSpec{TypeName: "assume_role", Nested: hcldec.ObjectSpec((*common.FlatAssumeRoleConfig)(nil).HCL2Spec())},
"custom_endpoint_ec2": &hcldec.AttrSpec{Name: "custom_endpoint_ec2", Type: cty.String, Required: false},
"shared_credentials_file": &hcldec.AttrSpec{Name: "shared_credentials_file", Type: cty.String, Required: false},
"decode_authorization_messages": &hcldec.AttrSpec{Name: "decode_authorization_messages", Type: cty.Bool, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false},
+10 -254
View File
@@ -1,5 +1,5 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type VaultAWSEngineOptions,AssumeRoleConfig
//go:generate mapstructure-to-hcl2 -type VaultAWSEngineOptions
package common
@@ -9,72 +9,17 @@ import (
"log"
"net/http"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
awsCredentials "github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/stscreds"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/ec2/ec2iface"
"github.com/aws/aws-sdk-go/service/sts"
cleanhttp "github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/packer/template/interpolate"
vaultapi "github.com/hashicorp/vault/api"
homedir "github.com/mitchellh/go-homedir"
)
// AssumeRoleConfig lets users set configuration options for assuming a special
// role when executing Packer.
//
// Usage example:
//
// HCL config example:
//
// ```HCL
// source "example" "amazon-ebs"{
// assume_role {
// role_arn = "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME"
// session_name = "SESSION_NAME"
// external_id = "EXTERNAL_ID"
// }
// }
// ```
//
// JSON config example:
//
// ```json
// builder{
// "type": "amazon-ebs",
// "assume_role": {
// "role_arn" : "arn:aws:iam::ACCOUNT_ID:role/ROLE_NAME",
// "session_name": "SESSION_NAME",
// "external_id" : "EXTERNAL_ID"
// }
// }
// ```
type AssumeRoleConfig struct {
// Amazon Resource Name (ARN) of the IAM Role to assume.
AssumeRoleARN string `mapstructure:"role_arn" required:"false"`
// Number of seconds to restrict the assume role session duration.
AssumeRoleDurationSeconds int `mapstructure:"duration_seconds" required:"false"`
// The external ID to use when assuming the role. If omitted, no external
// ID is passed to the AssumeRole call.
AssumeRoleExternalID string `mapstructure:"external_id" required:"false"`
// IAM Policy JSON describing further restricting permissions for the IAM
// Role being assumed.
AssumeRolePolicy string `mapstructure:"policy" required:"false"`
// Set of Amazon Resource Names (ARNs) of IAM Policies describing further
// restricting permissions for the IAM Role being
AssumeRolePolicyARNs []string `mapstructure:"policy_arns" required:"false"`
// Session name to use when assuming the role.
AssumeRoleSessionName string `mapstructure:"session_name" required:"false"`
// Map of assume role session tags.
AssumeRoleTags map[string]string `mapstructure:"tags" required:"false"`
// Set of assume role session tag keys to pass to any subsequent sessions.
AssumeRoleTransitiveTagKeys []string `mapstructure:"transitive_tag_keys" required:"false"`
}
type VaultAWSEngineOptions struct {
Name string `mapstructure:"name"`
RoleARN string `mapstructure:"role_arn"`
@@ -103,17 +48,10 @@ type AccessConfig struct {
// is not required if you are using `use_vault_aws_engine` for
// authentication instead.
AccessKey string `mapstructure:"access_key" required:"true"`
// If provided with a role ARN, Packer will attempt to assume this role
// using the supplied credentials. See
// [AssumeRoleConfig](#assume-role-configuration) below for more
// details on all of the options available, and for a usage example.
AssumeRole AssumeRoleConfig `mapstructure:"assume_role" required:"false"`
// 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.
CustomEndpointEc2 string `mapstructure:"custom_endpoint_ec2" required:"false"`
// Path to a credentials file to load credentials from
CredsFilename string `mapstructure:"shared_credentials_file" required:"false"`
// Enable automatic decoding of any encoded authorization (error) messages
// using the `sts:DecodeAuthorizationMessage` API. Note: requires that the
// effective user/role have permissions to `sts:DecodeAuthorizationMessage`
@@ -214,13 +152,16 @@ func (c *AccessConfig) Session() (*session.Session, error) {
return c.session, nil
}
// Create new AWS config
config := aws.NewConfig().WithCredentialsChainVerboseErrors(true)
if c.MaxRetries > 0 {
config = config.WithMaxRetries(c.MaxRetries)
}
// Set AWS config defaults.
staticCreds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token)
if _, err := staticCreds.Get(); err != credentials.ErrStaticCredentialsEmpty {
config.WithCredentials(staticCreds)
}
if c.RawRegion != "" {
config = config.WithRegion(c.RawRegion)
}
@@ -238,16 +179,6 @@ func (c *AccessConfig) Session() (*session.Session, error) {
}
transport.Proxy = http.ProxyFromEnvironment
// Figure out which possible credential providers are valid; test that we
// can get credentials via the selected providers, and set the providers in
// the config.
creds, err := c.GetCredentials(config)
if err != nil {
return nil, err
}
config.WithCredentials(creds)
// Create session options based on our AWS config
opts := session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: *config,
@@ -273,7 +204,9 @@ func (c *AccessConfig) Session() (*session.Session, error) {
cp, err := c.session.Config.Credentials.Get()
if IsAWSErr(err, "NoCredentialProviders", "") {
return nil, c.NewNoValidCredentialSourcesError(err)
return nil, fmt.Errorf("No valid credential sources found for AWS Builder. " +
"Please see https://www.packer.io/docs/builders/amazon#specifying-amazon-credentials " +
"for more information on providing credentials for the AWS Builder.")
}
if err != nil {
@@ -304,176 +237,6 @@ func (c *AccessConfig) IsChinaCloud() bool {
return strings.HasPrefix(c.SessionRegion(), "cn-")
}
// GetCredentials gets credentials from the environment, shared credentials,
// the session (which may include a credential process), or ECS/EC2 metadata
// endpoints. GetCredentials also validates the credentials and the ability to
// assume a role or will return an error if unsuccessful.
func (c *AccessConfig) GetCredentials(config *aws.Config) (*awsCredentials.Credentials, error) {
sharedCredentialsFilename, err := homedir.Expand(c.CredsFilename)
if err != nil {
return nil, fmt.Errorf("error expanding shared credentials filename: %w", err)
}
// Create a credentials chain that tries to load credentials from various
// common sources: config vars, then local profiles.
// Rather than using the default credentials chain, build a chain provider,
// lazy-evaluated by aws-sdk
providers := []awsCredentials.Provider{
// Tries to set new credentials object using the given
// access_key/secret_key/token. If they are not set, this will fail
// over to the other credentials providers
&awsCredentials.StaticProvider{Value: awsCredentials.Value{
AccessKeyID: c.AccessKey,
SecretAccessKey: c.SecretKey,
SessionToken: c.Token,
}},
// Tries to load credentials from environment.
&awsCredentials.EnvProvider{},
// Tries to load credentials from local file.
// If sharedCredentialsFilename is empty, the AWS sdk will use the
// environment var AWS_SHARED_CREDENTIALS_FILE to determine the file
// location, and if that's not set, AWS will use the default locations
// of:
// - Linux/Unix: $HOME/.aws/credentials
// - Windows: %USERPROFILE%\.aws\credentials
&awsCredentials.SharedCredentialsProvider{
Filename: sharedCredentialsFilename,
Profile: c.ProfileName,
},
}
// Validate the credentials before returning them
creds := awsCredentials.NewChainCredentials(providers)
cp, err := creds.Get()
if err != nil {
if IsAWSErr(err, "NoCredentialProviders", "") {
creds, err = c.GetCredentialsFromSession()
if err != nil {
return nil, err
}
}
return nil, fmt.Errorf("Error loading credentials for AWS Provider: %w", err)
}
log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName)
// In the "normal" flow (i.e. not assuming a role), we return here.
if c.AssumeRole.AssumeRoleARN == "" {
return creds, nil
}
// create a config for the assume role session based off the config we
// created for our main sessions
assumeRoleAWSConfig := config.Copy()
assumeRoleAWSConfig.CredentialsChainVerboseErrors = aws.Bool(true)
assumeRoleSession, err := session.NewSession(assumeRoleAWSConfig)
if err != nil {
return nil, fmt.Errorf("error creating assume role session: %w", err)
}
stsclient := sts.New(assumeRoleSession)
assumeRoleProvider := &stscreds.AssumeRoleProvider{
Client: stsclient,
RoleARN: c.AssumeRole.AssumeRoleARN,
}
if c.AssumeRole.AssumeRoleDurationSeconds > 0 {
assumeRoleProvider.Duration = time.Duration(c.AssumeRole.AssumeRoleDurationSeconds) * time.Second
}
if c.AssumeRole.AssumeRoleExternalID != "" {
assumeRoleProvider.ExternalID = aws.String(c.AssumeRole.AssumeRoleExternalID)
}
if c.AssumeRole.AssumeRolePolicy != "" {
assumeRoleProvider.Policy = aws.String(c.AssumeRole.AssumeRolePolicy)
}
if len(c.AssumeRole.AssumeRolePolicyARNs) > 0 {
var policyDescriptorTypes []*sts.PolicyDescriptorType
for _, policyARN := range c.AssumeRole.AssumeRolePolicyARNs {
policyDescriptorType := &sts.PolicyDescriptorType{
Arn: aws.String(policyARN),
}
policyDescriptorTypes = append(policyDescriptorTypes, policyDescriptorType)
}
assumeRoleProvider.PolicyArns = policyDescriptorTypes
}
if c.AssumeRole.AssumeRoleSessionName != "" {
assumeRoleProvider.RoleSessionName = c.AssumeRole.AssumeRoleSessionName
}
if len(c.AssumeRole.AssumeRoleTags) > 0 {
var tags []*sts.Tag
for k, v := range c.AssumeRole.AssumeRoleTags {
tag := &sts.Tag{
Key: aws.String(k),
Value: aws.String(v),
}
tags = append(tags, tag)
}
assumeRoleProvider.Tags = tags
}
if len(c.AssumeRole.AssumeRoleTransitiveTagKeys) > 0 {
assumeRoleProvider.TransitiveTagKeys = aws.StringSlice(c.AssumeRole.AssumeRoleTransitiveTagKeys)
}
providers = []awsCredentials.Provider{assumeRoleProvider}
assumeRoleCreds := awsCredentials.NewChainCredentials(providers)
_, err = assumeRoleCreds.Get()
if err != nil {
return nil, fmt.Errorf("Unable to assume role: %w", err)
}
return assumeRoleCreds, nil
}
// GetCredentialsFromSession returns credentials derived from a session. A
// session uses the AWS SDK Go chain of providers so may use a provider (e.g.,
// ProcessProvider) that is not part of the Terraform provider chain.
func (c *AccessConfig) GetCredentialsFromSession() (*awsCredentials.Credentials, error) {
log.Printf("[INFO] Attempting to use session-derived credentials")
// Avoid setting HTTPClient here as it will prevent the ec2metadata
// client from automatically lowering the timeout to 1 second.
options := &session.Options{
Config: aws.Config{
MaxRetries: aws.Int(0),
Region: aws.String(c.RawRegion),
},
Profile: c.ProfileName,
SharedConfigState: session.SharedConfigEnable,
}
sess, err := session.NewSessionWithOptions(*options)
if err != nil {
if IsAWSErr(err, "NoCredentialProviders", "") {
return nil, c.NewNoValidCredentialSourcesError(err)
}
return nil, fmt.Errorf("Error creating AWS session: %w", err)
}
creds := sess.Config.Credentials
cp, err := sess.Config.Credentials.Get()
if err != nil {
return nil, c.NewNoValidCredentialSourcesError(err)
}
log.Printf("[INFO] Successfully derived credentials from session")
log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName)
return creds, nil
}
func (c *AccessConfig) GetCredsFromVault() error {
// const EnvVaultAddress = "VAULT_ADDR"
// const EnvVaultToken = "VAULT_TOKEN"
@@ -543,13 +306,6 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
return errs
}
func (c *AccessConfig) NewNoValidCredentialSourcesError(err error) error {
return fmt.Errorf("No valid credential sources found for AWS Builder. "+
"Please see https://www.packer.io/docs/builders/amazon#authentication "+
"for more information on providing credentials for the AWS Builder. "+
"Error: %w", err)
}
func (c *AccessConfig) NewEC2Connection() (ec2iface.EC2API, error) {
if c.getEC2Connection != nil {
return c.getEC2Connection(), nil
@@ -1,4 +1,4 @@
// Code generated by "mapstructure-to-hcl2 -type VaultAWSEngineOptions,AssumeRoleConfig"; DO NOT EDIT.
// Code generated by "mapstructure-to-hcl2 -type VaultAWSEngineOptions"; DO NOT EDIT.
package common
import (
@@ -6,43 +6,6 @@ import (
"github.com/zclconf/go-cty/cty"
)
// FlatAssumeRoleConfig is an auto-generated flat version of AssumeRoleConfig.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatAssumeRoleConfig struct {
AssumeRoleARN *string `mapstructure:"role_arn" required:"false" cty:"role_arn" hcl:"role_arn"`
AssumeRoleDurationSeconds *int `mapstructure:"duration_seconds" required:"false" cty:"duration_seconds" hcl:"duration_seconds"`
AssumeRoleExternalID *string `mapstructure:"external_id" required:"false" cty:"external_id" hcl:"external_id"`
AssumeRolePolicy *string `mapstructure:"policy" required:"false" cty:"policy" hcl:"policy"`
AssumeRolePolicyARNs []string `mapstructure:"policy_arns" required:"false" cty:"policy_arns" hcl:"policy_arns"`
AssumeRoleSessionName *string `mapstructure:"session_name" required:"false" cty:"session_name" hcl:"session_name"`
AssumeRoleTags map[string]string `mapstructure:"tags" required:"false" cty:"tags" hcl:"tags"`
AssumeRoleTransitiveTagKeys []string `mapstructure:"transitive_tag_keys" required:"false" cty:"transitive_tag_keys" hcl:"transitive_tag_keys"`
}
// FlatMapstructure returns a new FlatAssumeRoleConfig.
// FlatAssumeRoleConfig is an auto-generated flat version of AssumeRoleConfig.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*AssumeRoleConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatAssumeRoleConfig)
}
// HCL2Spec returns the hcl spec of a AssumeRoleConfig.
// This spec is used by HCL to read the fields of AssumeRoleConfig.
// The decoded values from this spec will then be applied to a FlatAssumeRoleConfig.
func (*FlatAssumeRoleConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"role_arn": &hcldec.AttrSpec{Name: "role_arn", Type: cty.String, Required: false},
"duration_seconds": &hcldec.AttrSpec{Name: "duration_seconds", Type: cty.Number, Required: false},
"external_id": &hcldec.AttrSpec{Name: "external_id", Type: cty.String, Required: false},
"policy": &hcldec.AttrSpec{Name: "policy", Type: cty.String, Required: false},
"policy_arns": &hcldec.AttrSpec{Name: "policy_arns", Type: cty.List(cty.String), Required: false},
"session_name": &hcldec.AttrSpec{Name: "session_name", Type: cty.String, Required: false},
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
"transitive_tag_keys": &hcldec.AttrSpec{Name: "transitive_tag_keys", Type: cty.List(cty.String), Required: false},
}
return s
}
// FlatVaultAWSEngineOptions is an auto-generated flat version of VaultAWSEngineOptions.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatVaultAWSEngineOptions struct {
-11
View File
@@ -473,13 +473,6 @@ type RunConfig struct {
// terminating the tunnel it will automatically terminate itself after 20 minutes of inactivity.
SSHInterface string `mapstructure:"ssh_interface"`
// The time to wait before establishing the Session Manager session.
// The value of this should be a duration. Examples are
// `5s` and `1m30s` which will cause Packer to wait five seconds and one
// minute 30 seconds, respectively. If no set, defaults to 10 seconds.
// This option is useful when the remote port takes longer to become available.
PauseBeforeSSM time.Duration `mapstructure:"pause_before_ssm"`
// Which port to connect the local end of the session tunnel to. If
// left blank, Packer will choose a port for you from available ports.
// This option is only used when `ssh_interface` is set `session_manager`.
@@ -542,10 +535,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
msg := fmt.Errorf(`no iam_instance_profile defined; session_manager connectivity requires a valid instance profile with AmazonSSMManagedInstanceCore permissions. Alternatively a temporary_iam_instance_profile_policy_document can be used.`)
errs = append(errs, msg)
}
if c.PauseBeforeSSM == 0 {
c.PauseBeforeSSM = 10 * time.Second
}
}
if c.Comm.SSHKeyPairName != "" {
+3 -18
View File
@@ -46,7 +46,7 @@ func NewSSMDriver(config SSMDriverConfig) *SSMDriver {
// not wish to manage the session manually calling StopSession on a instance of this driver will terminate the active session
// created from calling StartSession.
func (d *SSMDriver) StartSession(ctx context.Context, input ssm.StartSessionInput) (*ssm.StartSessionOutput, error) {
log.Printf("Starting PortForwarding session to instance %q", aws.StringValue(input.Target))
log.Printf("Starting PortForwarding session to instance %q with following params %v", aws.StringValue(input.Target), input.Parameters)
var output *ssm.StartSessionOutput
err := retry.Config{
@@ -110,30 +110,15 @@ func (d *SSMDriver) openTunnelForSession(ctx context.Context) error {
select {
case <-ctx.Done():
return
case output, ok := <-stderrCh:
if !ok {
stderrCh = nil
break
}
case output := <-stderrCh:
if output != "" {
log.Printf("[ERROR] %s: %s", prefix, output)
}
case output, ok := <-stdoutCh:
if !ok {
stdoutCh = nil
break
}
case output := <-stdoutCh:
if output != "" {
log.Printf("[DEBUG] %s: %s", prefix, output)
}
}
if stdoutCh == nil && stderrCh == nil {
log.Printf("[DEBUG] %s: %s", prefix, "active session has been terminated; stopping all log polling processes.")
return
}
}
}(ctx, sessionManagerPluginName)
@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"strconv"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
@@ -22,7 +21,6 @@ type StepCreateSSMTunnel struct {
RemotePortNumber int
SSMAgentEnabled bool
instanceId string
PauseBeforeSSM time.Duration
driver *SSMDriver
}
@@ -34,17 +32,6 @@ func (s *StepCreateSSMTunnel) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionContinue
}
// Wait for the remote port to become available
if s.PauseBeforeSSM > 0 {
ui.Say(fmt.Sprintf("Waiting %s for establishing the SSM session...", s.PauseBeforeSSM))
select {
case <-time.After(s.PauseBeforeSSM):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}
// Configure local port number
if err := s.ConfigureLocalHostPort(ctx); err != nil {
err := fmt.Errorf("error finding an available port to initiate a session tunnel: %s", err)
+13 -10
View File
@@ -10,6 +10,7 @@ import (
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer/common/random"
"github.com/hashicorp/packer/common/retry"
@@ -280,6 +281,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
}
var createOutput *ec2.CreateFleetOutput
err = retry.Config{
Tries: 11,
ShouldRetry: func(err error) bool {
@@ -289,39 +291,40 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
// we can wait on those operations, this can be removed.
return true
}
return false
return request.IsErrorRetryable(err)
},
RetryDelay: (&retry.Backoff{InitialBackoff: 500 * time.Millisecond, MaxBackoff: 30 * time.Second, Multiplier: 2}).Linear,
}.Run(ctx, func(ctx context.Context) error {
createOutput, err = ec2conn.CreateFleet(createFleetInput)
if err == nil && createOutput.Errors != nil {
err = fmt.Errorf("errors: %v", createOutput.Errors)
}
// We can end up with errors because one of the allowed availability
// zones doesn't have one of the allowed instance types; as long as
// an instance is launched, these errors aren't important.
if len(createOutput.Instances) > 0 {
if err != nil {
log.Printf("create request failed for some instances %v", err.Error())
}
return nil
}
if err != nil {
log.Printf("create request failed %v", err)
}
return err
})
// Create the request for the spot instance.
if err != nil {
if createOutput.FleetId != nil {
err = fmt.Errorf("Error waiting for fleet request (%s): %s", *createOutput.FleetId, err)
} else {
err = fmt.Errorf("Error waiting for fleet request: %s", err)
}
// We can end up with errors because one of the allowed availability
// zones doesn't have one of the allowed instance types; as long as
// an instance is launched, these errors aren't important.
if len(createOutput.Errors) > 0 {
errString := fmt.Sprintf("Error waiting for fleet request (%s) to become ready:", *createOutput.FleetId)
for _, outErr := range createOutput.Errors {
errString = errString + aws.StringValue(outErr.ErrorMessage)
}
err = fmt.Errorf(errString)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("error", err)
ui.Error(err.Error())
-1
View File
@@ -269,7 +269,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&awscommon.StepCreateSSMTunnel{
AWSSession: session,
Region: *ec2conn.Config.Region,
PauseBeforeSSM: b.config.PauseBeforeSSM,
LocalPortNumber: b.config.SessionManagerPort,
RemotePortNumber: b.config.Comm.Port(),
SSMAgentEnabled: b.config.SSMAgentEnabled(),
-6
View File
@@ -19,9 +19,7 @@ type FlatConfig struct {
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
AssumeRole *common.FlatAssumeRoleConfig `mapstructure:"assume_role" required:"false" cty:"assume_role" hcl:"assume_role"`
CustomEndpointEc2 *string `mapstructure:"custom_endpoint_ec2" required:"false" cty:"custom_endpoint_ec2" hcl:"custom_endpoint_ec2"`
CredsFilename *string `mapstructure:"shared_credentials_file" required:"false" cty:"shared_credentials_file" hcl:"shared_credentials_file"`
DecodeAuthZMessages *bool `mapstructure:"decode_authorization_messages" required:"false" cty:"decode_authorization_messages" hcl:"decode_authorization_messages"`
InsecureSkipTLSVerify *bool `mapstructure:"insecure_skip_tls_verify" required:"false" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
MaxRetries *int `mapstructure:"max_retries" required:"false" cty:"max_retries" hcl:"max_retries"`
@@ -134,7 +132,6 @@ type FlatConfig struct {
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
SSHInterface *string `mapstructure:"ssh_interface" cty:"ssh_interface" hcl:"ssh_interface"`
PauseBeforeSSM *string `mapstructure:"pause_before_ssm" cty:"pause_before_ssm" hcl:"pause_before_ssm"`
SessionManagerPort *int `mapstructure:"session_manager_port" cty:"session_manager_port" hcl:"session_manager_port"`
AMIMappings []common.FlatBlockDevice `mapstructure:"ami_block_device_mappings" required:"false" cty:"ami_block_device_mappings" hcl:"ami_block_device_mappings"`
LaunchMappings []common.FlatBlockDevice `mapstructure:"launch_block_device_mappings" required:"false" cty:"launch_block_device_mappings" hcl:"launch_block_device_mappings"`
@@ -163,9 +160,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
"assume_role": &hcldec.BlockSpec{TypeName: "assume_role", Nested: hcldec.ObjectSpec((*common.FlatAssumeRoleConfig)(nil).HCL2Spec())},
"custom_endpoint_ec2": &hcldec.AttrSpec{Name: "custom_endpoint_ec2", Type: cty.String, Required: false},
"shared_credentials_file": &hcldec.AttrSpec{Name: "shared_credentials_file", Type: cty.String, Required: false},
"decode_authorization_messages": &hcldec.AttrSpec{Name: "decode_authorization_messages", Type: cty.Bool, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false},
@@ -278,7 +273,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"ssh_interface": &hcldec.AttrSpec{Name: "ssh_interface", Type: cty.String, Required: false},
"pause_before_ssm": &hcldec.AttrSpec{Name: "pause_before_ssm", Type: cty.String, Required: false},
"session_manager_port": &hcldec.AttrSpec{Name: "session_manager_port", Type: cty.Number, Required: false},
"ami_block_device_mappings": &hcldec.BlockListSpec{TypeName: "ami_block_device_mappings", Nested: hcldec.ObjectSpec((*common.FlatBlockDevice)(nil).HCL2Spec())},
"launch_block_device_mappings": &hcldec.BlockListSpec{TypeName: "launch_block_device_mappings", Nested: hcldec.ObjectSpec((*common.FlatBlockDevice)(nil).HCL2Spec())},
@@ -3,6 +3,9 @@
package ebssurrogate
import (
"strings"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/template/interpolate"
@@ -38,6 +41,50 @@ func (bds BlockDevices) BuildEC2BlockDeviceMappings() []*ec2.BlockDeviceMapping
return blockDevices
}
func (blockDevice BlockDevice) BuildEC2BlockDeviceMapping() *ec2.BlockDeviceMapping {
mapping := &ec2.BlockDeviceMapping{
DeviceName: aws.String(blockDevice.DeviceName),
}
if blockDevice.NoDevice {
mapping.NoDevice = aws.String("")
return mapping
} else if blockDevice.VirtualName != "" {
if strings.HasPrefix(blockDevice.VirtualName, "ephemeral") {
mapping.VirtualName = aws.String(blockDevice.VirtualName)
}
return mapping
}
ebsBlockDevice := &ec2.EbsBlockDevice{
DeleteOnTermination: aws.Bool(blockDevice.DeleteOnTermination),
}
if blockDevice.VolumeType != "" {
ebsBlockDevice.VolumeType = aws.String(blockDevice.VolumeType)
}
if blockDevice.VolumeSize > 0 {
ebsBlockDevice.VolumeSize = aws.Int64(blockDevice.VolumeSize)
}
// IOPS is only valid for io1 type
if blockDevice.VolumeType == "io1" {
ebsBlockDevice.Iops = aws.Int64(blockDevice.IOPS)
}
// You cannot specify Encrypted if you specify a Snapshot ID
if blockDevice.SnapshotId != "" {
ebsBlockDevice.SnapshotId = aws.String(blockDevice.SnapshotId)
}
ebsBlockDevice.Encrypted = blockDevice.Encrypted.ToBoolPointer()
mapping.Ebs = ebsBlockDevice
return mapping
}
func (bds BlockDevices) Prepare(ctx *interpolate.Context) (errs []error) {
for _, block := range bds {
if err := block.Prepare(ctx); err != nil {
-1
View File
@@ -293,7 +293,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&awscommon.StepCreateSSMTunnel{
AWSSession: session,
Region: *ec2conn.Config.Region,
PauseBeforeSSM: b.config.PauseBeforeSSM,
LocalPortNumber: b.config.SessionManagerPort,
RemotePortNumber: b.config.Comm.Port(),
SSMAgentEnabled: b.config.SSMAgentEnabled(),
@@ -62,9 +62,7 @@ type FlatConfig struct {
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
AssumeRole *common.FlatAssumeRoleConfig `mapstructure:"assume_role" required:"false" cty:"assume_role" hcl:"assume_role"`
CustomEndpointEc2 *string `mapstructure:"custom_endpoint_ec2" required:"false" cty:"custom_endpoint_ec2" hcl:"custom_endpoint_ec2"`
CredsFilename *string `mapstructure:"shared_credentials_file" required:"false" cty:"shared_credentials_file" hcl:"shared_credentials_file"`
DecodeAuthZMessages *bool `mapstructure:"decode_authorization_messages" required:"false" cty:"decode_authorization_messages" hcl:"decode_authorization_messages"`
InsecureSkipTLSVerify *bool `mapstructure:"insecure_skip_tls_verify" required:"false" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
MaxRetries *int `mapstructure:"max_retries" required:"false" cty:"max_retries" hcl:"max_retries"`
@@ -156,7 +154,6 @@ type FlatConfig struct {
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
SSHInterface *string `mapstructure:"ssh_interface" cty:"ssh_interface" hcl:"ssh_interface"`
PauseBeforeSSM *string `mapstructure:"pause_before_ssm" cty:"pause_before_ssm" hcl:"pause_before_ssm"`
SessionManagerPort *int `mapstructure:"session_manager_port" cty:"session_manager_port" hcl:"session_manager_port"`
AMIName *string `mapstructure:"ami_name" required:"true" cty:"ami_name" hcl:"ami_name"`
AMIDescription *string `mapstructure:"ami_description" required:"false" cty:"ami_description" hcl:"ami_description"`
@@ -207,9 +204,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
"assume_role": &hcldec.BlockSpec{TypeName: "assume_role", Nested: hcldec.ObjectSpec((*common.FlatAssumeRoleConfig)(nil).HCL2Spec())},
"custom_endpoint_ec2": &hcldec.AttrSpec{Name: "custom_endpoint_ec2", Type: cty.String, Required: false},
"shared_credentials_file": &hcldec.AttrSpec{Name: "shared_credentials_file", Type: cty.String, Required: false},
"decode_authorization_messages": &hcldec.AttrSpec{Name: "decode_authorization_messages", Type: cty.Bool, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false},
@@ -301,7 +296,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"ssh_interface": &hcldec.AttrSpec{Name: "ssh_interface", Type: cty.String, Required: false},
"pause_before_ssm": &hcldec.AttrSpec{Name: "pause_before_ssm", Type: cty.String, Required: false},
"session_manager_port": &hcldec.AttrSpec{Name: "session_manager_port", Type: cty.Number, Required: false},
"ami_name": &hcldec.AttrSpec{Name: "ami_name", Type: cty.String, Required: false},
"ami_description": &hcldec.AttrSpec{Name: "ami_description", Type: cty.String, Required: false},
-1
View File
@@ -263,7 +263,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&awscommon.StepCreateSSMTunnel{
AWSSession: session,
Region: *ec2conn.Config.Region,
PauseBeforeSSM: b.config.PauseBeforeSSM,
LocalPortNumber: b.config.SessionManagerPort,
RemotePortNumber: b.config.Comm.Port(),
SSMAgentEnabled: b.config.SSMAgentEnabled(),
@@ -64,9 +64,7 @@ type FlatConfig struct {
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
AssumeRole *common.FlatAssumeRoleConfig `mapstructure:"assume_role" required:"false" cty:"assume_role" hcl:"assume_role"`
CustomEndpointEc2 *string `mapstructure:"custom_endpoint_ec2" required:"false" cty:"custom_endpoint_ec2" hcl:"custom_endpoint_ec2"`
CredsFilename *string `mapstructure:"shared_credentials_file" required:"false" cty:"shared_credentials_file" hcl:"shared_credentials_file"`
DecodeAuthZMessages *bool `mapstructure:"decode_authorization_messages" required:"false" cty:"decode_authorization_messages" hcl:"decode_authorization_messages"`
InsecureSkipTLSVerify *bool `mapstructure:"insecure_skip_tls_verify" required:"false" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
MaxRetries *int `mapstructure:"max_retries" required:"false" cty:"max_retries" hcl:"max_retries"`
@@ -158,7 +156,6 @@ type FlatConfig struct {
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
SSHInterface *string `mapstructure:"ssh_interface" cty:"ssh_interface" hcl:"ssh_interface"`
PauseBeforeSSM *string `mapstructure:"pause_before_ssm" cty:"pause_before_ssm" hcl:"pause_before_ssm"`
SessionManagerPort *int `mapstructure:"session_manager_port" cty:"session_manager_port" hcl:"session_manager_port"`
AMIENASupport *bool `mapstructure:"ena_support" required:"false" cty:"ena_support" hcl:"ena_support"`
AMISriovNetSupport *bool `mapstructure:"sriov_support" required:"false" cty:"sriov_support" hcl:"sriov_support"`
@@ -187,9 +184,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
"assume_role": &hcldec.BlockSpec{TypeName: "assume_role", Nested: hcldec.ObjectSpec((*common.FlatAssumeRoleConfig)(nil).HCL2Spec())},
"custom_endpoint_ec2": &hcldec.AttrSpec{Name: "custom_endpoint_ec2", Type: cty.String, Required: false},
"shared_credentials_file": &hcldec.AttrSpec{Name: "shared_credentials_file", Type: cty.String, Required: false},
"decode_authorization_messages": &hcldec.AttrSpec{Name: "decode_authorization_messages", Type: cty.Bool, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false},
@@ -281,7 +276,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"ssh_interface": &hcldec.AttrSpec{Name: "ssh_interface", Type: cty.String, Required: false},
"pause_before_ssm": &hcldec.AttrSpec{Name: "pause_before_ssm", Type: cty.String, Required: false},
"session_manager_port": &hcldec.AttrSpec{Name: "session_manager_port", Type: cty.Number, Required: false},
"ena_support": &hcldec.AttrSpec{Name: "ena_support", Type: cty.Bool, Required: false},
"sriov_support": &hcldec.AttrSpec{Name: "sriov_support", Type: cty.Bool, Required: false},
-1
View File
@@ -343,7 +343,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&awscommon.StepCreateSSMTunnel{
AWSSession: session,
Region: *ec2conn.Config.Region,
PauseBeforeSSM: b.config.PauseBeforeSSM,
LocalPortNumber: b.config.SessionManagerPort,
RemotePortNumber: b.config.Comm.Port(),
SSMAgentEnabled: b.config.SSMAgentEnabled(),
@@ -19,9 +19,7 @@ type FlatConfig struct {
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
AssumeRole *common.FlatAssumeRoleConfig `mapstructure:"assume_role" required:"false" cty:"assume_role" hcl:"assume_role"`
CustomEndpointEc2 *string `mapstructure:"custom_endpoint_ec2" required:"false" cty:"custom_endpoint_ec2" hcl:"custom_endpoint_ec2"`
CredsFilename *string `mapstructure:"shared_credentials_file" required:"false" cty:"shared_credentials_file" hcl:"shared_credentials_file"`
DecodeAuthZMessages *bool `mapstructure:"decode_authorization_messages" required:"false" cty:"decode_authorization_messages" hcl:"decode_authorization_messages"`
InsecureSkipTLSVerify *bool `mapstructure:"insecure_skip_tls_verify" required:"false" cty:"insecure_skip_tls_verify" hcl:"insecure_skip_tls_verify"`
MaxRetries *int `mapstructure:"max_retries" required:"false" cty:"max_retries" hcl:"max_retries"`
@@ -134,7 +132,6 @@ type FlatConfig struct {
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
SSHInterface *string `mapstructure:"ssh_interface" cty:"ssh_interface" hcl:"ssh_interface"`
PauseBeforeSSM *string `mapstructure:"pause_before_ssm" cty:"pause_before_ssm" hcl:"pause_before_ssm"`
SessionManagerPort *int `mapstructure:"session_manager_port" cty:"session_manager_port" hcl:"session_manager_port"`
AMIMappings []common.FlatBlockDevice `mapstructure:"ami_block_device_mappings" required:"false" cty:"ami_block_device_mappings" hcl:"ami_block_device_mappings"`
LaunchMappings []common.FlatBlockDevice `mapstructure:"launch_block_device_mappings" required:"false" cty:"launch_block_device_mappings" hcl:"launch_block_device_mappings"`
@@ -169,9 +166,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
"assume_role": &hcldec.BlockSpec{TypeName: "assume_role", Nested: hcldec.ObjectSpec((*common.FlatAssumeRoleConfig)(nil).HCL2Spec())},
"custom_endpoint_ec2": &hcldec.AttrSpec{Name: "custom_endpoint_ec2", Type: cty.String, Required: false},
"shared_credentials_file": &hcldec.AttrSpec{Name: "shared_credentials_file", Type: cty.String, Required: false},
"decode_authorization_messages": &hcldec.AttrSpec{Name: "decode_authorization_messages", Type: cty.Bool, Required: false},
"insecure_skip_tls_verify": &hcldec.AttrSpec{Name: "insecure_skip_tls_verify", Type: cty.Bool, Required: false},
"max_retries": &hcldec.AttrSpec{Name: "max_retries", Type: cty.Number, Required: false},
@@ -284,7 +279,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"ssh_interface": &hcldec.AttrSpec{Name: "ssh_interface", Type: cty.String, Required: false},
"pause_before_ssm": &hcldec.AttrSpec{Name: "pause_before_ssm", Type: cty.String, Required: false},
"session_manager_port": &hcldec.AttrSpec{Name: "session_manager_port", Type: cty.Number, Required: false},
"ami_block_device_mappings": &hcldec.BlockListSpec{TypeName: "ami_block_device_mappings", Nested: hcldec.ObjectSpec((*common.FlatBlockDevice)(nil).HCL2Spec())},
"launch_block_device_mappings": &hcldec.BlockListSpec{TypeName: "launch_block_device_mappings", Nested: hcldec.ObjectSpec((*common.FlatBlockDevice)(nil).HCL2Spec())},
+13 -10
View File
@@ -288,7 +288,7 @@ type Config struct {
// Group, VM, NIC, VNET, Public IP, KeyVault, etc. The user can define up
// to 15 tags. Tag names cannot exceed 512 characters, and tag values
// cannot exceed 256 characters.
AzureTags map[string]string `mapstructure:"azure_tags" required:"false"`
AzureTags map[string]*string `mapstructure:"azure_tags" required:"false"`
// Same as [`azure_tags`](#azure_tags) but defined as a singular repeatable block
// containing a `name` and a `value` field. In HCL2 mode the
// [`dynamic_block`](/docs/configuration/from-1.5/expressions#dynamic-blocks)
@@ -513,7 +513,7 @@ func (c *Config) toImageParameters() *compute.Image {
},
},
Location: to.StringPtr(c.Location),
Tags: azcommon.MapToAzureTags(c.AzureTags),
Tags: c.AzureTags,
}
}
@@ -596,7 +596,10 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
// copy singular blocks
c.AzureTag.CopyOn(&c.AzureTags)
for _, kv := range c.AzureTag {
v := kv.Value
c.AzureTags[kv.Name] = &v
}
err = c.ClientConfig.SetDefaultValues()
if err != nil {
@@ -804,8 +807,8 @@ func assertTagProperties(c *Config, errs *packer.MultiError) {
if len(k) > 512 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 512 character limit", k, len(k)))
}
if len(v) > 256 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 256 character limit", v, len(v)))
if len(*v) > 256 {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("the tag name %q exceeds (%d) the 256 character limit", *v, len(*v)))
}
}
}
@@ -1060,13 +1063,13 @@ func assertRequiredParametersSet(c *Config, errs *packer.MultiError) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("if either plan_name, plan_product, plan_publisher, or plan_promotion_code are defined then plan_name, plan_product, and plan_publisher must be defined"))
} else {
if c.AzureTags == nil {
c.AzureTags = make(map[string]string)
c.AzureTags = make(map[string]*string)
}
c.AzureTags["PlanInfo"] = c.PlanInfo.PlanName
c.AzureTags["PlanProduct"] = c.PlanInfo.PlanProduct
c.AzureTags["PlanPublisher"] = c.PlanInfo.PlanPublisher
c.AzureTags["PlanPromotionCode"] = c.PlanInfo.PlanPromotionCode
c.AzureTags["PlanInfo"] = &c.PlanInfo.PlanName
c.AzureTags["PlanProduct"] = &c.PlanInfo.PlanProduct
c.AzureTags["PlanPublisher"] = &c.PlanInfo.PlanPublisher
c.AzureTags["PlanPromotionCode"] = &c.PlanInfo.PlanPromotionCode
}
}
+1 -1
View File
@@ -49,7 +49,7 @@ type FlatConfig struct {
ManagedImageOSDiskSnapshotName *string `mapstructure:"managed_image_os_disk_snapshot_name" required:"false" cty:"managed_image_os_disk_snapshot_name" hcl:"managed_image_os_disk_snapshot_name"`
ManagedImageDataDiskSnapshotPrefix *string `mapstructure:"managed_image_data_disk_snapshot_prefix" required:"false" cty:"managed_image_data_disk_snapshot_prefix" hcl:"managed_image_data_disk_snapshot_prefix"`
ManagedImageZoneResilient *bool `mapstructure:"managed_image_zone_resilient" required:"false" cty:"managed_image_zone_resilient" hcl:"managed_image_zone_resilient"`
AzureTags map[string]string `mapstructure:"azure_tags" required:"false" cty:"azure_tags" hcl:"azure_tags"`
AzureTags map[string]*string `mapstructure:"azure_tags" required:"false" cty:"azure_tags" hcl:"azure_tags"`
AzureTag []hcl2template.FlatNameValue `mapstructure:"azure_tag" required:"false" cty:"azure_tag" hcl:"azure_tag"`
ResourceGroupName *string `mapstructure:"resource_group_name" cty:"resource_group_name" hcl:"resource_group_name"`
StorageAccount *string `mapstructure:"storage_account" cty:"storage_account" hcl:"storage_account"`
+16 -41
View File
@@ -6,9 +6,7 @@ import (
"time"
"github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-04-01/compute"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/packer/builder/azure/common/constants"
"github.com/hashicorp/packer/hcl2template"
)
// List of configuration parameters that are required by the ARM builder.
@@ -912,55 +910,32 @@ func TestConfigShouldAcceptTags(t *testing.T) {
},
}
c := Config{
AzureTag: hcl2template.NameValues{
{Name: "tag03", Value: "value03"},
},
}
var c Config
_, err := c.Prepare(config, getPackerConfiguration())
if err != nil {
t.Fatal(err)
}
if diff := cmp.Diff(c.AzureTags, map[string]string{
"tag01": "value01",
"tag02": "value02",
"tag03": "value03",
}); diff != "" {
t.Fatalf("unexpected azure tags: %s", diff)
}
}
func TestConfigShouldAcceptTag(t *testing.T) {
config := map[string]interface{}{
"capture_name_prefix": "ignore",
"capture_container_name": "ignore",
"image_offer": "ignore",
"image_publisher": "ignore",
"image_sku": "ignore",
"location": "ignore",
"storage_account": "ignore",
"resource_group_name": "ignore",
"subscription_id": "ignore",
"communicator": "none",
// Does not matter for this test case, just pick one.
"os_type": constants.Target_Linux,
if len(c.AzureTags) != 2 {
t.Fatalf("expected to find 2 tags, but got %d", len(c.AzureTags))
}
c := Config{
AzureTag: hcl2template.NameValues{
{Name: "tag03", Value: "value03"},
},
if _, ok := c.AzureTags["tag01"]; !ok {
t.Error("expected to find key=\"tag01\", but did not")
}
_, err := c.Prepare(config, getPackerConfiguration())
if err != nil {
t.Fatal(err)
if _, ok := c.AzureTags["tag02"]; !ok {
t.Error("expected to find key=\"tag02\", but did not")
}
if diff := cmp.Diff(c.AzureTags, map[string]string{
"tag03": "value03",
}); diff != "" {
t.Fatalf("unexpected azure tags: %s", diff)
value := c.AzureTags["tag01"]
if *value != "value01" {
t.Errorf("expected AzureTags[\"tag01\"] to have value \"value01\", but got %q", *value)
}
value = c.AzureTags["tag02"]
if *value != "value02" {
t.Errorf("expected AzureTags[\"tag02\"] to have value \"value02\", but got %q", *value)
}
}
-10
View File
@@ -1,10 +0,0 @@
package common
func MapToAzureTags(in map[string]string) map[string]*string {
res := map[string]*string{}
for k := range in {
v := in[k]
res[k] = &v
}
return res
}
+10 -10
View File
@@ -25,16 +25,16 @@ type Parameters struct {
/////////////////////////////////////////////////
// Template > Resource
type Resource struct {
ApiVersion *string `json:"apiVersion"`
Name *string `json:"name"`
Type *string `json:"type"`
Location *string `json:"location,omitempty"`
DependsOn *[]string `json:"dependsOn,omitempty"`
Plan *Plan `json:"plan,omitempty"`
Properties *Properties `json:"properties,omitempty"`
Tags *map[string]string `json:"tags,omitempty"`
Resources *[]Resource `json:"resources,omitempty"`
Identity *Identity `json:"identity,omitempty"`
ApiVersion *string `json:"apiVersion"`
Name *string `json:"name"`
Type *string `json:"type"`
Location *string `json:"location,omitempty"`
DependsOn *[]string `json:"dependsOn,omitempty"`
Plan *Plan `json:"plan,omitempty"`
Properties *Properties `json:"properties,omitempty"`
Tags *map[string]*string `json:"tags,omitempty"`
Resources *[]Resource `json:"resources,omitempty"`
Identity *Identity `json:"identity,omitempty"`
}
type Plan struct {
@@ -374,7 +374,7 @@ func (s *TemplateBuilder) SetNetworkSecurityGroup(ipAddresses []string, port int
return nil
}
func (s *TemplateBuilder) SetTags(tags *map[string]string) error {
func (s *TemplateBuilder) SetTags(tags *map[string]*string) error {
if tags == nil || len(*tags) == 0 {
return nil
}
+11 -19
View File
@@ -5,7 +5,6 @@ import (
"log"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/builder"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
@@ -30,9 +29,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
return nil, warnings, errs
}
return []string{
"ImageSha256",
}, warnings, nil
return nil, warnings, nil
}
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
@@ -47,16 +44,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
}
log.Printf("[DEBUG] Docker version: %s", version.String())
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("hook", hook)
state.Put("ui", ui)
generatedData := &builder.GeneratedData{State: state}
// Setup the driver that will talk to Docker
state.Put("driver", driver)
steps := []multistep.Step{
&StepTempDir{},
&StepPull{},
@@ -80,11 +67,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
log.Print("[DEBUG] Container will be discarded")
} else if b.config.Commit {
log.Print("[DEBUG] Container will be committed")
steps = append(steps,
new(StepCommit),
&StepSetGeneratedData{ // Adds ImageSha256 variable available after StepCommit
GeneratedData: generatedData,
})
steps = append(steps, new(StepCommit))
} else if b.config.ExportPath != "" {
log.Printf("[DEBUG] Container will be exported to %s", b.config.ExportPath)
steps = append(steps, new(StepExport))
@@ -92,6 +75,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
return nil, errArtifactNotUsed
}
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("hook", hook)
state.Put("ui", ui)
// Setup the driver that will talk to Docker
state.Put("driver", driver)
// Run!
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
-3
View File
@@ -26,9 +26,6 @@ type Driver interface {
// for external access.
IPAddress(id string) (string, error)
// Sha256 returns the sha256 id of the image
Sha256(id string) (string, error)
// Login. This will lock the driver from performing another Login
// until Logout is called. Therefore, any users MUST call Logout.
Login(repo, username, password string) error
-17
View File
@@ -159,23 +159,6 @@ func (d *DockerDriver) IPAddress(id string) (string, error) {
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) Sha256(id string) (string, error) {
var stderr, stdout bytes.Buffer
cmd := exec.Command(
"docker",
"inspect",
"--format",
"{{ .Id }}",
id)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("Error: %s\n\nStderr: %s", err, stderr.String())
}
return strings.TrimSpace(stdout.String()), nil
}
func (d *DockerDriver) Login(repo, user, pass string) error {
d.l.Lock()
-11
View File
@@ -28,11 +28,6 @@ type MockDriver struct {
IPAddressResult string
IPAddressErr error
Sha256Called bool
Sha256Id string
Sha256Result string
Sha256Err error
KillCalled bool
KillID string
KillError error
@@ -123,12 +118,6 @@ func (d *MockDriver) IPAddress(id string) (string, error) {
return d.IPAddressResult, d.IPAddressErr
}
func (d *MockDriver) Sha256(id string) (string, error) {
d.Sha256Called = true
d.Sha256Id = id
return d.Sha256Result, d.Sha256Err
}
func (d *MockDriver) Login(r, u, p string) error {
d.LoginCalled = true
d.LoginRepo = r
-30
View File
@@ -1,30 +0,0 @@
package docker
import (
"context"
"github.com/hashicorp/packer/builder"
"github.com/hashicorp/packer/helper/multistep"
)
type StepSetGeneratedData struct {
GeneratedData *builder.GeneratedData
}
func (s *StepSetGeneratedData) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
sha256 := "ERR_IMAGE_SHA256_NOT_FOUND"
if imageId, ok := state.GetOk("image_id"); ok {
s256, err := driver.Sha256(imageId.(string))
if err == nil {
sha256 = s256
}
}
s.GeneratedData.Put("ImageSha256", sha256)
return multistep.ActionContinue
}
func (s *StepSetGeneratedData) Cleanup(_ multistep.StateBag) {
// No cleanup...
}
@@ -1,51 +0,0 @@
package docker
import (
"context"
"testing"
"github.com/hashicorp/packer/builder"
"github.com/hashicorp/packer/helper/multistep"
)
func TestStepSetGeneratedData_Run(t *testing.T) {
state := testState(t)
step := new(StepSetGeneratedData)
step.GeneratedData = &builder.GeneratedData{State: state}
driver := state.Get("driver").(*MockDriver)
driver.Sha256Result = "80B3BB1B1696E73A9B19DEEF92F664F8979F948DF348088B61F9A3477655AF64"
state.Put("image_id", "12345")
if action := step.Run(context.TODO(), state); action != multistep.ActionContinue {
t.Fatalf("Should not halt")
}
if !driver.Sha256Called {
t.Fatalf("driver.SHA256 should be called")
}
if driver.Sha256Id != "12345" {
t.Fatalf("driver.SHA256 got wrong image it: %s", driver.Sha256Id)
}
genData := state.Get("generated_data").(map[string]interface{})
imgSha256 := genData["ImageSha256"].(string)
if imgSha256 != driver.Sha256Result {
t.Fatalf("Expected ImageSha256 to be %s but was %s", driver.Sha256Result, imgSha256)
}
// Image ID not implement
state = testState(t)
step.GeneratedData = &builder.GeneratedData{State: state}
driver = state.Get("driver").(*MockDriver)
notImplementedMsg := "ERR_IMAGE_SHA256_NOT_FOUND"
if action := step.Run(context.TODO(), state); action != multistep.ActionContinue {
t.Fatalf("Should not halt")
}
if driver.Sha256Called {
t.Fatalf("driver.SHA256 should not be called")
}
genData = state.Get("generated_data").(map[string]interface{})
imgSha256 = genData["ImageSha256"].(string)
if imgSha256 != notImplementedMsg {
t.Fatalf("Expected ImageSha256 to be %s but was %s", notImplementedMsg, imgSha256)
}
}
+2 -3
View File
@@ -15,7 +15,6 @@ import (
"time"
compute "google.golang.org/api/compute/v1"
"google.golang.org/api/option"
oslogin "google.golang.org/api/oslogin/v1"
"github.com/hashicorp/packer/common/retry"
@@ -125,12 +124,12 @@ func NewDriverGCE(ui packer.Ui, p string, conf *jwt.Config, vaultOauth string) (
}
log.Printf("[INFO] Instantiating GCE client...")
service, err := compute.NewService(context.TODO(), option.WithHTTPClient(client))
service, err := compute.New(client)
if err != nil {
return nil, err
}
osLoginService, err := oslogin.NewService(context.TODO(), option.WithHTTPClient(client))
osLoginService, err := oslogin.New(client)
if err != nil {
return nil, err
}
+1 -4
View File
@@ -294,10 +294,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
GuestAdditionsPath: b.config.GuestAdditionsPath,
Generation: b.config.Generation,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
&hypervcommon.StepMountSecondaryDvdImages{
IsoPaths: b.config.SecondaryDvdImages,
Generation: b.config.Generation,
-59
View File
@@ -3,20 +3,16 @@
package openstack
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack"
"github.com/gophercloud/utils/openstack/clientconfig"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
@@ -240,15 +236,6 @@ func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
return nil
}
func (c *AccessConfig) enableDebug(ui packer.Ui) {
c.osClient.HTTPClient = http.Client{
Transport: &DebugRoundTripper{
ui: ui,
rt: c.osClient.HTTPClient.Transport,
},
}
}
func (c *AccessConfig) computeV2Client() (*gophercloud.ServiceClient, error) {
return openstack.NewComputeV2(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
@@ -286,49 +273,3 @@ func (c *AccessConfig) getEndpointType() gophercloud.Availability {
}
return gophercloud.AvailabilityPublic
}
type DebugRoundTripper struct {
ui packer.Ui
rt http.RoundTripper
numReauthAttempts int
}
// RoundTrip performs a round-trip HTTP request and logs relevant information about it.
func (drt *DebugRoundTripper) RoundTrip(request *http.Request) (*http.Response, error) {
defer func() {
if request.Body != nil {
request.Body.Close()
}
}()
var response *http.Response
var err error
response, err = drt.rt.RoundTrip(request)
if response == nil {
return nil, err
}
if response.StatusCode == http.StatusUnauthorized {
if drt.numReauthAttempts == 3 {
return response, fmt.Errorf("Tried to re-authenticate 3 times with no success.")
}
drt.numReauthAttempts++
}
drt.DebugMessage(fmt.Sprintf("Request %s %s %d", request.Method, request.URL, response.StatusCode))
if response.StatusCode >= 400 {
buf := bytes.NewBuffer([]byte{})
body, _ := ioutil.ReadAll(io.TeeReader(response.Body, buf))
drt.DebugMessage(fmt.Sprintf("Response Error: %+v\n", string(body)))
bufWithClose := ioutil.NopCloser(buf)
response.Body = bufWithClose
}
return response, err
}
func (drt *DebugRoundTripper) DebugMessage(message string) {
drt.ui.Message(fmt.Sprintf("[DEBUG] %s", message))
}
+5 -11
View File
@@ -71,10 +71,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
}
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
if b.config.PackerDebug {
b.config.enableDebug(ui)
}
computeClient, err := b.config.computeV2Client()
if err != nil {
return nil, fmt.Errorf("Error initializing compute client: %s", err)
@@ -102,13 +98,11 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
DebugKeyPath: fmt.Sprintf("os_%s.pem", b.config.PackerBuildName),
},
&StepSourceImageInfo{
SourceImage: b.config.RunConfig.SourceImage,
SourceImageName: b.config.RunConfig.SourceImageName,
ExternalSourceImageURL: b.config.RunConfig.ExternalSourceImageURL,
ExternalSourceImageFormat: b.config.RunConfig.ExternalSourceImageFormat,
SourceImageOpts: b.config.RunConfig.sourceImageOpts,
SourceMostRecent: b.config.SourceImageFilters.MostRecent,
SourceProperties: b.config.SourceImageFilters.Filters.Properties,
SourceImage: b.config.RunConfig.SourceImage,
SourceImageName: b.config.RunConfig.SourceImageName,
SourceImageOpts: b.config.RunConfig.sourceImageOpts,
SourceMostRecent: b.config.SourceImageFilters.MostRecent,
SourceProperties: b.config.SourceImageFilters.Filters.Properties,
},
&StepDiscoverNetwork{
Networks: b.config.Networks,
-4
View File
@@ -95,8 +95,6 @@ type FlatConfig struct {
SSHIPVersion *string `mapstructure:"ssh_ip_version" required:"false" cty:"ssh_ip_version" hcl:"ssh_ip_version"`
SourceImage *string `mapstructure:"source_image" required:"true" cty:"source_image" hcl:"source_image"`
SourceImageName *string `mapstructure:"source_image_name" required:"true" cty:"source_image_name" hcl:"source_image_name"`
ExternalSourceImageURL *string `mapstructure:"external_source_image_url" required:"true" cty:"external_source_image_url" hcl:"external_source_image_url"`
ExternalSourceImageFormat *string `mapstructure:"external_source_image_format" required:"false" cty:"external_source_image_format" hcl:"external_source_image_format"`
SourceImageFilters *FlatImageFilter `mapstructure:"source_image_filter" required:"true" cty:"source_image_filter" hcl:"source_image_filter"`
Flavor *string `mapstructure:"flavor" required:"true" cty:"flavor" hcl:"flavor"`
AvailabilityZone *string `mapstructure:"availability_zone" required:"false" cty:"availability_zone" hcl:"availability_zone"`
@@ -222,8 +220,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"ssh_ip_version": &hcldec.AttrSpec{Name: "ssh_ip_version", Type: cty.String, Required: false},
"source_image": &hcldec.AttrSpec{Name: "source_image", Type: cty.String, Required: false},
"source_image_name": &hcldec.AttrSpec{Name: "source_image_name", Type: cty.String, Required: false},
"external_source_image_url": &hcldec.AttrSpec{Name: "external_source_image_url", Type: cty.String, Required: false},
"external_source_image_format": &hcldec.AttrSpec{Name: "external_source_image_format", Type: cty.String, Required: false},
"source_image_filter": &hcldec.BlockSpec{TypeName: "source_image_filter", Nested: hcldec.ObjectSpec((*FlatImageFilter)(nil).HCL2Spec())},
"flavor": &hcldec.AttrSpec{Name: "flavor", Type: cty.String, Required: false},
"availability_zone": &hcldec.AttrSpec{Name: "availability_zone", Type: cty.String, Required: false},
+7 -26
View File
@@ -33,11 +33,6 @@ type RunConfig struct {
// The name of the base image to use. This is an alternative way of
// providing source_image and only either of them can be specified.
SourceImageName string `mapstructure:"source_image_name" required:"true"`
// The URL of an external base image to use. This is an alternative way of
// providing source_image and only either of them can be specified.
ExternalSourceImageURL string `mapstructure:"external_source_image_url" required:"true"`
// The format of the external source image to use, e.g. qcow2, raw.
ExternalSourceImageFormat string `mapstructure:"external_source_image_format" required:"false"`
// Filters used to populate filter options. Example:
//
// ```json
@@ -252,19 +247,10 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
hasOnlySourceImage := len(c.SourceImage) > 0 && len(c.SourceImageName) == 0 && len(c.ExternalSourceImageURL) == 0
hasOnlySourceImageName := len(c.SourceImageName) > 0 && len(c.SourceImage) == 0 && len(c.ExternalSourceImageURL) == 0
hasOnlyExternalSourceImageURL := len(c.ExternalSourceImageURL) > 0 && len(c.SourceImage) == 0 && len(c.SourceImageName) == 0
if c.SourceImage == "" && c.SourceImageName == "" && c.ExternalSourceImageURL == "" && c.SourceImageFilters.Filters.Empty() {
errs = append(errs, errors.New("Either a source_image, a source_image_name, an external_source_image_url or source_image_filter must be specified"))
} else if !(hasOnlySourceImage || hasOnlySourceImageName || hasOnlyExternalSourceImageURL) {
errs = append(errs, errors.New("Only a source_image, a source_image_name or an external_source_image_url can be specified, not multiple."))
}
// if external_source_image_format is not set use qcow2 as default
if c.ExternalSourceImageFormat == "" {
c.ExternalSourceImageFormat = "qcow2"
if c.SourceImage == "" && c.SourceImageName == "" && c.SourceImageFilters.Filters.Empty() {
errs = append(errs, errors.New("Either a source_image, a source_image_name, or source_image_filter must be specified"))
} else if len(c.SourceImage) > 0 && len(c.SourceImageName) > 0 {
errs = append(errs, errors.New("Only a source_image or a source_image_name can be specified, not both."))
}
if c.Flavor == "" {
@@ -297,9 +283,9 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
// if neither ID, image name or external image URL is provided outside the filter,
// build the filter
if len(c.SourceImage) == 0 && len(c.SourceImageName) == 0 && len(c.ExternalSourceImageURL) == 0 {
// if neither ID or image name is provided outside the filter, build the
// filter
if len(c.SourceImage) == 0 && len(c.SourceImageName) == 0 {
listOpts, filterErr := c.SourceImageFilters.Filters.Build()
@@ -309,11 +295,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
c.sourceImageOpts = *listOpts
}
// if c.ExternalSourceImageURL is set use a generated source image name
if c.ExternalSourceImageURL != "" {
c.SourceImageName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
return errs
}
-44
View File
@@ -2,7 +2,6 @@ package openstack
import (
"os"
"regexp"
"testing"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
@@ -133,49 +132,6 @@ func TestRunConfigPrepare_FloatingIPPoolCompat(t *testing.T) {
}
}
func TestRunConfigPrepare_ExternalSourceImageURL(t *testing.T) {
c := testRunConfig()
// test setting both ExternalSourceImageURL and SourceImage causes an error
c.ExternalSourceImageURL = "http://example.com/image.qcow2"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
// test setting both ExternalSourceImageURL and SourceImageName causes an error
c.SourceImage = ""
c.SourceImageName = "abcd"
c.ExternalSourceImageURL = "http://example.com/image.qcow2"
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
// test neither setting SourceImage, SourceImageName or ExternalSourceImageURL causes an error
c.SourceImage = ""
c.SourceImageName = ""
c.ExternalSourceImageURL = ""
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("err: %s", err)
}
// test setting only ExternalSourceImageURL passes
c.SourceImage = ""
c.SourceImageName = ""
c.ExternalSourceImageURL = "http://example.com/image.qcow2"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
// test default values
if c.ExternalSourceImageFormat != "qcow2" {
t.Fatalf("ExternalSourceImageFormat should have been set to default: qcow2")
}
p := `packer_[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}`
if matches, _ := regexp.MatchString(p, c.SourceImageName); !matches {
t.Fatalf("invalid format for SourceImageName: %s", c.SourceImageName)
}
}
// This test case confirms that only allowed fields will be set to values
// The checked values are non-nil for their target type
func TestBuildImageFilter(t *testing.T) {
+12 -94
View File
@@ -4,9 +4,7 @@ import (
"context"
"fmt"
"log"
"time"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/imageimport"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/gophercloud/gophercloud/pagination"
"github.com/hashicorp/packer/helper/multistep"
@@ -14,13 +12,11 @@ import (
)
type StepSourceImageInfo struct {
SourceImage string
SourceImageName string
ExternalSourceImageURL string
ExternalSourceImageFormat string
SourceImageOpts images.ListOpts
SourceMostRecent bool
SourceProperties map[string]string
SourceImage string
SourceImageName string
SourceImageOpts images.ListOpts
SourceMostRecent bool
SourceProperties map[string]string
}
func PropertiesSatisfied(image *images.Image, props *map[string]string) bool {
@@ -37,6 +33,12 @@ func (s *StepSourceImageInfo) Run(ctx context.Context, state multistep.StateBag)
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
if s.SourceImage != "" {
state.Put("source_image", s.SourceImage)
return multistep.ActionContinue
}
client, err := config.imageV2Client()
if err != nil {
err := fmt.Errorf("error creating image client: %s", err)
@@ -45,70 +47,6 @@ func (s *StepSourceImageInfo) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionHalt
}
if s.ExternalSourceImageURL != "" {
createOpts := images.CreateOpts{
Name: s.SourceImageName,
ContainerFormat: "bare",
DiskFormat: s.ExternalSourceImageFormat,
Properties: map[string]string{
"packer_external_source_image_url": s.ExternalSourceImageURL,
"packer_external_source_image_format": s.ExternalSourceImageFormat,
},
}
ui.Say("Creating image using external source image with name " + s.SourceImageName)
ui.Say("Using disk format " + s.ExternalSourceImageFormat)
image, err := images.Create(client, createOpts).Extract()
if err != nil {
err := fmt.Errorf("Error creating source image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Created image with ID " + image.ID)
importOpts := imageimport.CreateOpts{
Name: imageimport.WebDownloadMethod,
URI: s.ExternalSourceImageURL,
}
ui.Say("Importing External Source Image from URL " + s.ExternalSourceImageURL)
err = imageimport.Create(client, image.ID, importOpts).ExtractErr()
if err != nil {
err := fmt.Errorf("Error importing source image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for image.Status != images.ImageStatusActive {
ui.Message("Image not Active, retrying in 10 seconds")
time.Sleep(10 * time.Second)
img, err := images.Get(client, image.ID).Extract()
if err != nil {
err := fmt.Errorf("Error querying image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
image = img
}
s.SourceImage = image.ID
}
if s.SourceImage != "" {
state.Put("source_image", s.SourceImage)
return multistep.ActionContinue
}
if s.SourceImageName != "" {
s.SourceImageOpts = images.ListOpts{
Name: s.SourceImageName,
@@ -179,25 +117,5 @@ func (s *StepSourceImageInfo) Run(ctx context.Context, state multistep.StateBag)
}
func (s *StepSourceImageInfo) Cleanup(state multistep.StateBag) {
if s.ExternalSourceImageURL != "" {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
client, err := config.imageV2Client()
if err != nil {
err := fmt.Errorf("error creating image client: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return
}
ui.Say(fmt.Sprintf("Deleting temporary external source image: %s ...", s.SourceImageName))
err = images.Delete(client, s.SourceImage).ExtractErr()
if err != nil {
err := fmt.Errorf("error cleaning up external source image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return
}
}
// No cleanup required for backout
}
-12
View File
@@ -86,7 +86,6 @@ type diskConfig struct {
Size string `mapstructure:"disk_size"`
CacheMode string `mapstructure:"cache_mode"`
DiskFormat string `mapstructure:"format"`
IOThread bool `mapstructure:"io_thread"`
}
type vgaConfig struct {
Type string `mapstructure:"type"`
@@ -190,17 +189,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
log.Printf("Disk %d cache mode not set, using default 'none'", idx)
c.Disks[idx].CacheMode = "none"
}
if c.Disks[idx].IOThread {
// io thread is only supported by virtio-scsi-single controller
if c.SCSIController != "virtio-scsi-single" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("io thread option requires virtio-scsi-single controller"))
} else {
// ... and only for virtio and scsi disks
if !(c.Disks[idx].Type == "scsi" || c.Disks[idx].Type == "virtio") {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("io thread option requires scsi or a virtio disk"))
}
}
}
// For any storage pool types which aren't in rxStorageTypes in proxmox-api/proxmox/config_qemu.go:890
// (currently zfspool|lvm|rbd|cephfs), the format parameter is mandatory. Make sure this is still up to date
// when updating the vendored code!
-2
View File
@@ -230,7 +230,6 @@ type FlatdiskConfig struct {
Size *string `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
CacheMode *string `mapstructure:"cache_mode" cty:"cache_mode" hcl:"cache_mode"`
DiskFormat *string `mapstructure:"format" cty:"format" hcl:"format"`
IOThread *bool `mapstructure:"io_thread" cty:"io_thread" hcl:"io_thread"`
}
// FlatMapstructure returns a new FlatdiskConfig.
@@ -251,7 +250,6 @@ func (*FlatdiskConfig) HCL2Spec() map[string]hcldec.Spec {
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.String, Required: false},
"cache_mode": &hcldec.AttrSpec{Name: "cache_mode", Type: cty.String, Required: false},
"format": &hcldec.AttrSpec{Name: "format", Type: cty.String, Required: false},
"io_thread": &hcldec.AttrSpec{Name: "io_thread", Type: cty.Bool, Required: false},
}
return s
}
-49
View File
@@ -208,52 +208,3 @@ func TestPacketQueueSupportForNetworkAdapters(t *testing.T) {
}
}
}
func TestHardDiskControllerIOThreadSupport(t *testing.T) {
drivertests := []struct {
expectedToFail bool
controller string
disk_type string
}{
// io thread is only supported by virtio-scsi-single controller
// and only for virtio and scsi disks
{expectedToFail: false, controller: "virtio-scsi-single", disk_type: "scsi"},
{expectedToFail: false, controller: "virtio-scsi-single", disk_type: "virtio"},
{expectedToFail: true, controller: "virtio-scsi-single", disk_type: "sata"},
{expectedToFail: true, controller: "lsi", disk_type: "scsi"},
{expectedToFail: true, controller: "lsi53c810", disk_type: "virtio"},
}
for _, tt := range drivertests {
nic := make(map[string]interface{})
nic["bridge"] = "vmbr0"
nics := make([]map[string]interface{}, 0)
nics = append(nics, nic)
disk := make(map[string]interface{})
disk["type"] = tt.disk_type
disk["io_thread"] = true
disk["storage_pool"] = "local-lvm"
disk["storage_pool_type"] = "lvm"
disks := make([]map[string]interface{}, 0)
disks = append(disks, disk)
cfg := mandatoryConfig(t)
cfg["network_adapters"] = nics
cfg["disks"] = disks
cfg["scsi_controller"] = tt.controller
var c Config
_, err := c.Prepare(cfg)
if tt.expectedToFail == true && err == nil {
t.Error("expected config preparation to fail, but no error occured")
}
if tt.expectedToFail == false && err != nil {
t.Errorf("expected config preparation to succeed, but %s", err.Error())
}
}
}
-4
View File
@@ -142,10 +142,6 @@ func generateProxmoxDisks(disks []diskConfig) proxmox.QemuDevices {
setDeviceParamIfDefined(devs[idx], "storage_type", disks[idx].StoragePoolType)
setDeviceParamIfDefined(devs[idx], "cache", disks[idx].CacheMode)
setDeviceParamIfDefined(devs[idx], "format", disks[idx].DiskFormat)
if devs[idx]["type"] == "scsi" || devs[idx]["type"] == "virtio" {
setDeviceParamIfDefined(devs[idx], "iothread", strconv.FormatBool(disks[idx].IOThread))
}
}
return devs
}
+607 -61
View File
@@ -1,3 +1,6 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type Config
package qemu
import (
@@ -8,26 +11,570 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/common/shutdowncommand"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
const BuilderId = "transcend.qemu"
var accels = map[string]struct{}{
"none": {},
"kvm": {},
"tcg": {},
"xen": {},
"hax": {},
"hvf": {},
"whpx": {},
}
var diskInterface = map[string]bool{
"ide": true,
"scsi": true,
"virtio": true,
"virtio-scsi": true,
}
var diskCache = map[string]bool{
"writethrough": true,
"writeback": true,
"none": true,
"unsafe": true,
"directsync": true,
}
var diskDiscard = map[string]bool{
"unmap": true,
"ignore": true,
}
var diskDZeroes = map[string]bool{
"unmap": true,
"on": true,
"off": true,
}
type Builder struct {
config Config
runner multistep.Runner
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
bootcommand.VNCConfig `mapstructure:",squash"`
shutdowncommand.ShutdownConfig `mapstructure:",squash"`
CommConfig CommConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
// Use iso from provided url. Qemu must support
// curl block device. This defaults to `false`.
ISOSkipCache bool `mapstructure:"iso_skip_cache" required:"false"`
// The accelerator type to use when running the VM.
// This may be `none`, `kvm`, `tcg`, `hax`, `hvf`, `whpx`, or `xen`. The appropriate
// software must have already been installed on your build machine to use the
// accelerator you specified. When no accelerator is specified, Packer will try
// to use `kvm` if it is available but will default to `tcg` otherwise.
//
// ~> The `hax` accelerator has issues attaching CDROM ISOs. This is an
// upstream issue which can be tracked
// [here](https://github.com/intel/haxm/issues/20).
//
// ~> The `hvf` and `whpx` accelerator are new and experimental as of
// [QEMU 2.12.0](https://wiki.qemu.org/ChangeLog/2.12#Host_support).
// You may encounter issues unrelated to Packer when using these. You may need to
// add [ "-global", "virtio-pci.disable-modern=on" ] to `qemuargs` depending on the
// guest operating system.
//
// ~> For `whpx`, note that [Stefan Weil's QEMU for Windows distribution](https://qemu.weilnetz.de/w64/)
// does not include WHPX support and users may need to compile or source a
// build of QEMU for Windows themselves with WHPX support.
Accelerator string `mapstructure:"accelerator" required:"false"`
// Additional disks to create. Uses `vm_name` as the disk name template and
// appends `-#` where `#` is the position in the array. `#` starts at 1 since 0
// is the default disk. Each string represents the disk image size in bytes.
// Optional suffixes 'k' or 'K' (kilobyte, 1024), 'M' (megabyte, 1024k), 'G'
// (gigabyte, 1024M), 'T' (terabyte, 1024G), 'P' (petabyte, 1024T) and 'E'
// (exabyte, 1024P) are supported. 'b' is ignored. Per qemu-img documentation.
// Each additional disk uses the same disk parameters as the default disk.
// Unset by default.
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false"`
// The number of cpus to use when building the VM.
// The default is `1` CPU.
CpuCount int `mapstructure:"cpus" required:"false"`
// The interface to use for the disk. Allowed values include any of `ide`,
// `scsi`, `virtio` or `virtio-scsi`^\*. Note also that any boot commands
// or kickstart type scripts must have proper adjustments for resulting
// device names. The Qemu builder uses `virtio` by default.
//
// ^\* Please be aware that use of the `scsi` disk interface has been
// disabled by Red Hat due to a bug described
// [here](https://bugzilla.redhat.com/show_bug.cgi?id=1019220). If you are
// running Qemu on RHEL or a RHEL variant such as CentOS, you *must* choose
// one of the other listed interfaces. Using the `scsi` interface under
// these circumstances will cause the build to fail.
DiskInterface string `mapstructure:"disk_interface" required:"false"`
// The size in bytes of the hard disk of the VM. Suffix with the first
// letter of common byte types. Use "k" or "K" for kilobytes, "M" for
// megabytes, G for gigabytes, and T for terabytes. If no value is provided
// for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size
// number is provided with no units, Packer will default to Megabytes.
DiskSize string `mapstructure:"disk_size" required:"false"`
// Packer resizes the QCOW2 image using
// qemu-img resize. Set this option to true to disable resizing.
// Defaults to false.
SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"`
// The cache mode to use for disk. Allowed values include any of
// `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By
// default, this is set to `writeback`.
DiskCache string `mapstructure:"disk_cache" required:"false"`
// The discard mode to use for disk. Allowed values
// include any of unmap or ignore. By default, this is set to ignore.
DiskDiscard string `mapstructure:"disk_discard" required:"false"`
// The detect-zeroes mode to use for disk.
// Allowed values include any of unmap, on or off. Defaults to off.
// When the value is "off" we don't set the flag in the qemu command, so that
// Packer still works with old versions of QEMU that don't have this option.
DetectZeroes string `mapstructure:"disk_detect_zeroes" required:"false"`
// Packer compacts the QCOW2 image using
// qemu-img convert. Set this option to true to disable compacting.
// Defaults to false.
SkipCompaction bool `mapstructure:"skip_compaction" required:"false"`
// Apply compression to the QCOW2 disk file
// using qemu-img convert. Defaults to false.
DiskCompression bool `mapstructure:"disk_compression" required:"false"`
// Either `qcow2` or `raw`, this specifies the output format of the virtual
// machine image. This defaults to `qcow2`.
Format string `mapstructure:"format" required:"false"`
// Packer defaults to building QEMU virtual machines by
// launching a GUI that shows the console of the machine being built. When this
// value is set to `true`, the machine will start without a console.
//
// You can still see the console if you make a note of the VNC display
// number chosen, and then connect using `vncviewer -Shared <host>:<display>`
Headless bool `mapstructure:"headless" required:"false"`
// Packer defaults to building from an ISO file, this parameter controls
// whether the ISO URL supplied is actually a bootable QEMU image. When
// this value is set to `true`, the machine will either clone the source or
// use it as a backing file (if `use_backing_file` is `true`); then, it
// will resize the image according to `disk_size` and boot it.
DiskImage bool `mapstructure:"disk_image" required:"false"`
// Only applicable when disk_image is true
// and format is qcow2, set this option to true to create a new QCOW2
// file that uses the file located at iso_url as a backing file. The new file
// will only contain blocks that have changed compared to the backing file, so
// enabling this option can significantly reduce disk usage.
UseBackingFile bool `mapstructure:"use_backing_file" required:"false"`
// The type of machine emulation to use. Run your qemu binary with the
// flags `-machine help` to list available types for your system. This
// defaults to `pc`.
MachineType string `mapstructure:"machine_type" required:"false"`
// The amount of memory to use when building the VM
// in megabytes. This defaults to 512 megabytes.
MemorySize int `mapstructure:"memory" required:"false"`
// The driver to use for the network interface. Allowed values `ne2k_pci`,
// `i82551`, `i82557b`, `i82559er`, `rtl8139`, `e1000`, `pcnet`, `virtio`,
// `virtio-net`, `virtio-net-pci`, `usb-net`, `i82559a`, `i82559b`,
// `i82559c`, `i82550`, `i82562`, `i82557a`, `i82557c`, `i82801`,
// `vmxnet3`, `i82558a` or `i82558b`. The Qemu builder uses `virtio-net` by
// default.
NetDevice string `mapstructure:"net_device" required:"false"`
// Connects the network to this bridge instead of using the user mode
// networking.
//
// **NB** This bridge must already exist. You can use the `virbr0` bridge
// as created by vagrant-libvirt.
//
// **NB** This will automatically enable the QMP socket (see QMPEnable).
//
// **NB** This only works in Linux based OSes.
NetBridge string `mapstructure:"net_bridge" required:"false"`
// This is the path to the directory where the
// resulting virtual machine will be created. This may be relative or absolute.
// If relative, the path is relative to the working directory when packer
// is executed. This directory must not exist or be empty prior to running
// the builder. By default this is output-BUILDNAME where "BUILDNAME" is the
// name of the build.
OutputDir string `mapstructure:"output_directory" required:"false"`
// Allows complete control over the qemu command line (though not, at this
// time, qemu-img). Each array of strings makes up a command line switch
// that overrides matching default switch/value pairs. Any value specified
// as an empty string is ignored. All values after the switch are
// concatenated with no separator.
//
// ~> **Warning:** The qemu command line allows extreme flexibility, so
// beware of conflicting arguments causing failures of your run. For
// instance, using --no-acpi could break the ability to send power signal
// type commands (e.g., shutdown -P now) to the virtual machine, thus
// preventing proper shutdown. To see the defaults, look in the packer.log
// file and search for the qemu-system-x86 command. The arguments are all
// printed for review.
//
// The following shows a sample usage:
//
// In JSON:
// ```json
// "qemuargs": [
// [ "-m", "1024M" ],
// [ "--no-acpi", "" ],
// [
// "-netdev",
// "user,id=mynet0,",
// "hostfwd=hostip:hostport-guestip:guestport",
// ""
// ],
// [ "-device", "virtio-net,netdev=mynet0" ]
// ]
// ```
//
// In HCL2:
// ```hcl
// qemuargs = [
// [ "-m", "1024M" ],
// [ "--no-acpi", "" ],
// [
// "-netdev",
// "user,id=mynet0,",
// "hostfwd=hostip:hostport-guestip:guestport",
// ""
// ],
// [ "-device", "virtio-net,netdev=mynet0" ]
// ]
// ```
//
// would produce the following (not including other defaults supplied by
// the builder and not otherwise conflicting with the qemuargs):
//
// ```text
// qemu-system-x86 -m 1024m --no-acpi -netdev
// user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device
// virtio-net,netdev=mynet0"
// ```
//
// ~> **Windows Users:** [QEMU for Windows](https://qemu.weilnetz.de/)
// builds are available though an environmental variable does need to be
// set for QEMU for Windows to redirect stdout to the console instead of
// stdout.txt.
//
// The following shows the environment variable that needs to be set for
// Windows QEMU support:
//
// ```text
// setx SDL_STDIO_REDIRECT=0
// ```
//
// You can also use the `SSHHostPort` template variable to produce a packer
// template that can be invoked by `make` in parallel:
//
// In JSON:
// ```json
// "qemuargs": [
// [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"],
// [ "-device", "virtio-net,netdev=forward,id=net0"]
// ]
// ```
//
// In HCL2:
// ```hcl
// qemuargs = [
// [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"],
// [ "-device", "virtio-net,netdev=forward,id=net0"]
// ]
//
// `make -j 3 my-awesome-packer-templates` spawns 3 packer processes, each
// of which will bind to their own SSH port as determined by each process.
// This will also work with WinRM, just change the port forward in
// `qemuargs` to map to WinRM's default port of `5985` or whatever value
// you have the service set to listen on.
//
// This is a template engine and allows access to the following variables:
// `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`,
// `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false"`
// The name of the Qemu binary to look for. This
// defaults to qemu-system-x86_64, but may need to be changed for
// some platforms. For example qemu-kvm, or qemu-system-i386 may be a
// better choice for some systems.
QemuBinary string `mapstructure:"qemu_binary" required:"false"`
// Enable QMP socket. Location is specified by `qmp_socket_path`. Defaults
// to false.
QMPEnable bool `mapstructure:"qmp_enable" required:"false"`
// QMP Socket Path when `qmp_enable` is true. Defaults to
// `output_directory`/`vm_name`.monitor.
QMPSocketPath string `mapstructure:"qmp_socket_path" required:"false"`
// If true, do not pass a -display option
// to qemu, allowing it to choose the default. This may be needed when running
// under macOS, and getting errors about sdl not being available.
UseDefaultDisplay bool `mapstructure:"use_default_display" required:"false"`
// What QEMU -display option to use. Defaults to gtk, use none to not pass the
// -display option allowing QEMU to choose the default. This may be needed when
// running under macOS, and getting errors about sdl not being available.
Display string `mapstructure:"display" required:"false"`
// The IP address that should be
// binded to for VNC. By default packer will use 127.0.0.1 for this. If you
// wish to bind to all interfaces use 0.0.0.0.
VNCBindAddress string `mapstructure:"vnc_bind_address" required:"false"`
// Whether or not to set a password on the VNC server. This option
// automatically enables the QMP socket. See `qmp_socket_path`. Defaults to
// `false`.
VNCUsePassword bool `mapstructure:"vnc_use_password" required:"false"`
// The minimum and maximum port
// to use for VNC access to the virtual machine. The builder uses VNC to type
// the initial boot_command. Because Packer generally runs in parallel,
// Packer uses a randomly chosen port in this range that appears available. By
// default this is 5900 to 6000. The minimum and maximum ports are inclusive.
VNCPortMin int `mapstructure:"vnc_port_min" required:"false"`
VNCPortMax int `mapstructure:"vnc_port_max"`
// This is the name of the image (QCOW2 or IMG) file for
// the new virtual machine. By default this is packer-BUILDNAME, where
// "BUILDNAME" is the name of the build. Currently, no file extension will be
// used unless it is specified in this option.
VMName string `mapstructure:"vm_name" required:"false"`
// The interface to use for the CDROM device which contains the ISO image.
// Allowed values include any of `ide`, `scsi`, `virtio` or
// `virtio-scsi`. The Qemu builder uses `virtio` by default.
// Some ARM64 images require `virtio-scsi`.
CDROMInterface string `mapstructure:"cdrom_interface" required:"false"`
// TODO(mitchellh): deprecate
RunOnce bool `mapstructure:"run_once"`
ctx interpolate.Context
}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
warnings, errs := b.config.Prepare(raws...)
if errs != nil {
err := config.Decode(&b.config, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &b.config.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"boot_command",
"qemuargs",
},
},
}, raws...)
if err != nil {
return nil, nil, err
}
var errs *packer.MultiError
warnings := make([]string, 0)
errs = packer.MultiErrorAppend(errs, b.config.ShutdownConfig.Prepare(&b.config.ctx)...)
if b.config.DiskSize == "" || b.config.DiskSize == "0" {
b.config.DiskSize = "40960M"
} else {
// Make sure supplied disk size is valid
// (digits, plus an optional valid unit character). e.g. 5000, 40G, 1t
re := regexp.MustCompile(`^[\d]+(b|k|m|g|t){0,1}$`)
matched := re.MatchString(strings.ToLower(b.config.DiskSize))
if !matched {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Invalid disk size."))
} else {
// Okay, it's valid -- if it doesn't alreay have a suffix, then
// append "M" as the default unit.
re = regexp.MustCompile(`^[\d]+$`)
matched = re.MatchString(strings.ToLower(b.config.DiskSize))
if matched {
// Needs M added.
b.config.DiskSize = fmt.Sprintf("%sM", b.config.DiskSize)
}
}
}
if b.config.DiskCache == "" {
b.config.DiskCache = "writeback"
}
if b.config.DiskDiscard == "" {
b.config.DiskDiscard = "ignore"
}
if b.config.DetectZeroes == "" {
b.config.DetectZeroes = "off"
}
if b.config.Accelerator == "" {
if runtime.GOOS == "windows" {
b.config.Accelerator = "tcg"
} else {
// /dev/kvm is a kernel module that may be loaded if kvm is
// installed and the host supports VT-x extensions. To make sure
// this will actually work we need to os.Open() it. If os.Open fails
// the kernel module was not installed or loaded correctly.
if fp, err := os.Open("/dev/kvm"); err != nil {
b.config.Accelerator = "tcg"
} else {
fp.Close()
b.config.Accelerator = "kvm"
}
}
log.Printf("use detected accelerator: %s", b.config.Accelerator)
} else {
log.Printf("use specified accelerator: %s", b.config.Accelerator)
}
if b.config.MachineType == "" {
b.config.MachineType = "pc"
}
if b.config.OutputDir == "" {
b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
}
if b.config.QemuBinary == "" {
b.config.QemuBinary = "qemu-system-x86_64"
}
if b.config.MemorySize < 10 {
log.Printf("MemorySize %d is too small, using default: 512", b.config.MemorySize)
b.config.MemorySize = 512
}
if b.config.CpuCount < 1 {
log.Printf("CpuCount %d too small, using default: 1", b.config.CpuCount)
b.config.CpuCount = 1
}
if b.config.VNCBindAddress == "" {
b.config.VNCBindAddress = "127.0.0.1"
}
if b.config.VNCPortMin == 0 {
b.config.VNCPortMin = 5900
}
if b.config.VNCPortMax == 0 {
b.config.VNCPortMax = 6000
}
if b.config.VMName == "" {
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
}
if b.config.Format == "" {
b.config.Format = "qcow2"
}
errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.VNCConfig.Prepare(&b.config.ctx)...)
if b.config.NetDevice == "" {
b.config.NetDevice = "virtio-net"
}
if b.config.DiskInterface == "" {
b.config.DiskInterface = "virtio"
}
if b.config.ISOSkipCache {
b.config.ISOChecksum = "none"
}
isoWarnings, isoErrs := b.config.ISOConfig.Prepare(&b.config.ctx)
warnings = append(warnings, isoWarnings...)
errs = packer.MultiErrorAppend(errs, isoErrs...)
errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
commConfigWarnings, es := b.config.CommConfig.Prepare(&b.config.ctx)
if len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
warnings = append(warnings, commConfigWarnings...)
if !(b.config.Format == "qcow2" || b.config.Format == "raw") {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
}
if b.config.Format != "qcow2" {
b.config.SkipCompaction = true
b.config.DiskCompression = false
}
if b.config.UseBackingFile && !(b.config.DiskImage && b.config.Format == "qcow2") {
errs = packer.MultiErrorAppend(
errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true"))
}
if b.config.DiskImage && len(b.config.AdditionalDiskSize) > 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("disk_additional_size can only be used when disk_image is false"))
}
if b.config.SkipResizeDisk && !(b.config.DiskImage) {
errs = packer.MultiErrorAppend(
errs, errors.New("skip_resize_disk can only be used when disk_image is true"))
}
if _, ok := accels[b.config.Accelerator]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
}
if _, ok := diskInterface[b.config.DiskInterface]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk interface type"))
}
if _, ok := diskCache[b.config.DiskCache]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk cache type"))
}
if _, ok := diskDiscard[b.config.DiskDiscard]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk discard type"))
}
if _, ok := diskDZeroes[b.config.DetectZeroes]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk detect zeroes setting"))
}
if !b.config.PackerForce {
if _, err := os.Stat(b.config.OutputDir); err == nil {
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Output directory '%s' already exists. It must not exist.", b.config.OutputDir))
}
}
if b.config.VNCPortMin > b.config.VNCPortMax {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if b.config.NetBridge != "" && runtime.GOOS != "linux" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("net_bridge is only supported in Linux based OSes"))
}
if b.config.NetBridge != "" || b.config.VNCUsePassword {
b.config.QMPEnable = true
}
if b.config.QMPEnable && b.config.QMPSocketPath == "" {
socketName := fmt.Sprintf("%s.monitor", b.config.VMName)
b.config.QMPSocketPath = filepath.Join(b.config.OutputDir, socketName)
}
if b.config.QemuArgs == nil {
b.config.QemuArgs = make([][]string, 0)
}
if errs != nil && len(errs.Errors) > 0 {
return nil, warnings, errs
}
@@ -41,6 +588,15 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
return nil, fmt.Errorf("Failed creating Qemu driver: %s", err)
}
steprun := &stepRun{}
if !b.config.DiskImage {
steprun.BootDrive = "once=d"
steprun.Message = "Starting VM, booting from CD-ROM"
} else {
steprun.BootDrive = "c"
steprun.Message = "Starting VM, booting disk image"
}
steps := []multistep.Step{}
if !b.config.ISOSkipCache {
steps = append(steps, &common.StepDownload{
@@ -50,12 +606,14 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
ResultKey: "iso_path",
TargetPath: b.config.TargetPath,
Url: b.config.ISOUrls,
})
},
)
} else {
steps = append(steps, &stepSetISO{
ResultKey: "iso_path",
Url: b.config.ISOUrls,
})
},
)
}
steps = append(steps, new(stepPrepareOutputDir),
@@ -64,37 +622,9 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Directories: b.config.FloppyConfig.FloppyDirectories,
Label: b.config.FloppyConfig.FloppyLabel,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
&stepCreateDisk{
AdditionalDiskSize: b.config.AdditionalDiskSize,
DiskImage: b.config.DiskImage,
DiskSize: b.config.DiskSize,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
UseBackingFile: b.config.UseBackingFile,
VMName: b.config.VMName,
QemuImgArgs: b.config.QemuImgArgs,
},
&stepCopyDisk{
DiskImage: b.config.DiskImage,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
UseBackingFile: b.config.UseBackingFile,
VMName: b.config.VMName,
},
&stepResizeDisk{
DiskCompression: b.config.DiskCompression,
DiskImage: b.config.DiskImage,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
SkipResizeDisk: b.config.SkipResizeDisk,
VMName: b.config.VMName,
DiskSize: b.config.DiskSize,
QemuImgArgs: b.config.QemuImgArgs,
},
new(stepCreateDisk),
new(stepCopyDisk),
new(stepResizeDisk),
new(stepHTTPIPDiscover),
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
@@ -102,42 +632,58 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
HTTPPortMax: b.config.HTTPPortMax,
HTTPAddress: b.config.HTTPAddress,
},
&stepPortForward{
CommunicatorType: b.config.CommConfig.Comm.Type,
NetBridge: b.config.NetBridge,
},
)
if b.config.CommConfig.Comm.Type != "none" && b.config.NetBridge == "" {
steps = append(steps,
new(stepPortForward),
)
}
steps = append(steps,
new(stepConfigureVNC),
&stepRun{
DiskImage: b.config.DiskImage,
},
steprun,
&stepConfigureQMP{
QMPSocketPath: b.config.QMPSocketPath,
},
&stepTypeBootCommand{},
&stepWaitGuestAddress{
CommunicatorType: b.config.CommConfig.Comm.Type,
NetBridge: b.config.NetBridge,
timeout: b.config.CommConfig.Comm.SSHTimeout,
},
&communicator.StepConnect{
Config: &b.config.CommConfig.Comm,
Host: commHost(b.config.CommConfig.Comm.Host()),
SSHConfig: b.config.CommConfig.Comm.SSHConfigFunc(),
SSHPort: commPort,
WinRMPort: commPort,
},
)
if b.config.CommConfig.Comm.Type != "none" && b.config.NetBridge != "" {
steps = append(steps,
&stepWaitGuestAddress{
timeout: b.config.CommConfig.Comm.SSHTimeout,
},
)
}
if b.config.CommConfig.Comm.Type != "none" {
steps = append(steps,
&communicator.StepConnect{
Config: &b.config.CommConfig.Comm,
Host: commHost(b.config.CommConfig.Comm.Host()),
SSHConfig: b.config.CommConfig.Comm.SSHConfigFunc(),
SSHPort: commPort,
WinRMPort: commPort,
},
)
}
steps = append(steps,
new(common.StepProvision),
)
steps = append(steps,
&common.StepCleanupTempKeys{
Comm: &b.config.CommConfig.Comm,
},
)
steps = append(steps,
new(stepShutdown),
&stepConvertDisk{
DiskCompression: b.config.DiskCompression,
Format: b.config.Format,
OutputDir: b.config.OutputDir,
SkipCompaction: b.config.SkipCompaction,
VMName: b.config.VMName,
},
)
steps = append(steps,
new(stepConvertDisk),
)
// Setup the state bag
@@ -1,4 +1,4 @@
// Code generated by "mapstructure-to-hcl2 -type Config,QemuImgArgs"; DO NOT EDIT.
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
package qemu
import (
@@ -88,8 +88,6 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
ISOSkipCache *bool `mapstructure:"iso_skip_cache" required:"false" cty:"iso_skip_cache" hcl:"iso_skip_cache"`
Accelerator *string `mapstructure:"accelerator" required:"false" cty:"accelerator" hcl:"accelerator"`
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false" cty:"disk_additional_size" hcl:"disk_additional_size"`
@@ -112,7 +110,6 @@ type FlatConfig struct {
NetBridge *string `mapstructure:"net_bridge" required:"false" cty:"net_bridge" hcl:"net_bridge"`
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false" cty:"qemuargs" hcl:"qemuargs"`
QemuImgArgs *FlatQemuImgArgs `mapstructure:"qemu_img_args" required:"false" cty:"qemu_img_args" hcl:"qemu_img_args"`
QemuBinary *string `mapstructure:"qemu_binary" required:"false" cty:"qemu_binary" hcl:"qemu_binary"`
QMPEnable *bool `mapstructure:"qmp_enable" required:"false" cty:"qmp_enable" hcl:"qmp_enable"`
QMPSocketPath *string `mapstructure:"qmp_socket_path" required:"false" cty:"qmp_socket_path" hcl:"qmp_socket_path"`
@@ -218,8 +215,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"iso_skip_cache": &hcldec.AttrSpec{Name: "iso_skip_cache", Type: cty.Bool, Required: false},
"accelerator": &hcldec.AttrSpec{Name: "accelerator", Type: cty.String, Required: false},
"disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.String), Required: false},
@@ -242,7 +237,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"net_bridge": &hcldec.AttrSpec{Name: "net_bridge", Type: cty.String, Required: false},
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
"qemuargs": &hcldec.AttrSpec{Name: "qemuargs", Type: cty.List(cty.List(cty.String)), Required: false},
"qemu_img_args": &hcldec.BlockSpec{TypeName: "qemu_img_args", Nested: hcldec.ObjectSpec((*FlatQemuImgArgs)(nil).HCL2Spec())},
"qemu_binary": &hcldec.AttrSpec{Name: "qemu_binary", Type: cty.String, Required: false},
"qmp_enable": &hcldec.AttrSpec{Name: "qmp_enable", Type: cty.Bool, Required: false},
"qmp_socket_path": &hcldec.AttrSpec{Name: "qmp_socket_path", Type: cty.String, Required: false},
@@ -258,30 +252,3 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
}
return s
}
// FlatQemuImgArgs is an auto-generated flat version of QemuImgArgs.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatQemuImgArgs struct {
Convert []string `mapstructure:"convert" required:"false" cty:"convert" hcl:"convert"`
Create []string `mapstructure:"create" required:"false" cty:"create" hcl:"create"`
Resize []string `mapstructure:"resize" required:"false" cty:"resize" hcl:"resize"`
}
// FlatMapstructure returns a new FlatQemuImgArgs.
// FlatQemuImgArgs is an auto-generated flat version of QemuImgArgs.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*QemuImgArgs) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatQemuImgArgs)
}
// HCL2Spec returns the hcl spec of a QemuImgArgs.
// This spec is used by HCL to read the fields of QemuImgArgs.
// The decoded values from this spec will then be applied to a FlatQemuImgArgs.
func (*FlatQemuImgArgs) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"convert": &hcldec.AttrSpec{Name: "convert", Type: cty.List(cty.String), Required: false},
"create": &hcldec.AttrSpec{Name: "create", Type: cty.List(cty.String), Required: false},
"resize": &hcldec.AttrSpec{Name: "resize", Type: cty.List(cty.String), Required: false},
}
return s
}
+658
View File
@@ -1,11 +1,56 @@
package qemu
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/hashicorp/packer/packer"
)
var testPem = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
-----END RSA PRIVATE KEY-----
`
func testConfig() map[string]interface{} {
return map[string]interface{}{
"iso_checksum": "md5:0B0F137F17AC10944716020B018F8126",
"iso_url": "http://www.google.com/",
"ssh_username": "foo",
packer.BuildNameConfigKey: "foo",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
@@ -13,3 +58,616 @@ func TestBuilder_ImplementsBuilder(t *testing.T) {
t.Error("Builder must implement builder.")
}
}
func TestBuilderPrepare_Defaults(t *testing.T) {
var b Builder
config := testConfig()
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.OutputDir != "output-foo" {
t.Errorf("bad output dir: %s", b.config.OutputDir)
}
if b.config.CommConfig.HostPortMin != 2222 {
t.Errorf("bad min ssh host port: %d", b.config.CommConfig.HostPortMin)
}
if b.config.CommConfig.HostPortMax != 4444 {
t.Errorf("bad max ssh host port: %d", b.config.CommConfig.HostPortMax)
}
if b.config.CommConfig.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", b.config.CommConfig.Comm.SSHPort)
}
if b.config.VMName != "packer-foo" {
t.Errorf("bad vm name: %s", b.config.VMName)
}
if b.config.Format != "qcow2" {
t.Errorf("bad format: %s", b.config.Format)
}
}
func TestBuilderPrepare_VNCBindAddress(t *testing.T) {
var b Builder
config := testConfig()
// Test a default boot_wait
delete(config, "vnc_bind_address")
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if b.config.VNCBindAddress != "127.0.0.1" {
t.Fatalf("bad value: %s", b.config.VNCBindAddress)
}
}
func TestBuilderPrepare_DiskCompaction(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["skip_compaction"] = false
config["disk_compression"] = true
config["format"] = "img"
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
if b.config.SkipCompaction != true {
t.Fatalf("SkipCompaction should be true")
}
if b.config.DiskCompression != false {
t.Fatalf("DiskCompression should be false")
}
// Good
config["skip_compaction"] = false
config["disk_compression"] = true
config["format"] = "qcow2"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SkipCompaction != false {
t.Fatalf("SkipCompaction should be false")
}
if b.config.DiskCompression != true {
t.Fatalf("DiskCompression should be true")
}
}
func TestBuilderPrepare_DiskSize(t *testing.T) {
type testcase struct {
InputSize string
OutputSize string
ErrExpected bool
}
testCases := []testcase{
{"", "40960M", false}, // not provided
{"12345", "12345M", false}, // no unit given, defaults to M
{"12345x", "12345x", true}, // invalid unit
{"12345T", "12345T", false}, // terabytes
{"12345b", "12345b", false}, // bytes get preserved when set.
{"60000M", "60000M", false}, // Original test case
}
for _, tc := range testCases {
// Set input disk size
var b Builder
config := testConfig()
delete(config, "disk_size")
config["disk_size"] = tc.InputSize
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if (err == nil) == tc.ErrExpected {
t.Fatalf("bad: error when providing disk size %s; Err expected: %t; err recieved: %v", tc.InputSize, tc.ErrExpected, err)
}
if b.config.DiskSize != tc.OutputSize {
t.Fatalf("bad size: received: %s but expected %s", b.config.DiskSize, tc.OutputSize)
}
}
}
func TestBuilderPrepare_AdditionalDiskSize(t *testing.T) {
var b Builder
config := testConfig()
config["disk_additional_size"] = []string{"1M"}
config["disk_image"] = true
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatalf("should have error")
}
delete(config, "disk_image")
config["disk_additional_size"] = []string{"1M"}
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.AdditionalDiskSize[0] != "1M" {
t.Fatalf("bad size: %s", b.config.AdditionalDiskSize)
}
}
func TestBuilderPrepare_Format(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["format"] = "illegal value"
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["format"] = "qcow2"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Good
config["format"] = "raw"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_UseBackingFile(t *testing.T) {
var b Builder
config := testConfig()
config["use_backing_file"] = true
// Bad: iso_url is not a disk_image
config["disk_image"] = false
config["format"] = "qcow2"
b = Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad: format is not 'qcow2'
config["disk_image"] = true
config["format"] = "raw"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good: iso_url is a disk image and format is 'qcow2'
config["disk_image"] = true
config["format"] = "qcow2"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SkipResizeDisk(t *testing.T) {
config := testConfig()
config["skip_resize_disk"] = true
config["disk_image"] = false
b := Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Errorf("unexpected warns when calling prepare with skip_resize_disk set to true: %#v", warns)
}
if err == nil {
t.Errorf("setting skip_resize_disk to true when disk_image is false should have error")
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "floppy_files")
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("bad err: %s", err)
}
if len(b.config.FloppyFiles) != 0 {
t.Fatalf("bad: %#v", b.config.FloppyFiles)
}
floppies_path := "../../common/test-fixtures/floppies"
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
if !reflect.DeepEqual(b.config.FloppyFiles, expected) {
t.Fatalf("bad: %#v", b.config.FloppyFiles)
}
}
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
var b Builder
config := testConfig()
config["floppy_files"] = []string{"nonexistent.bat", "nonexistent.ps1"}
b = Builder{}
_, _, errs := b.Prepare(config)
if errs == nil {
t.Fatalf("Nonexistent floppies should trigger multierror")
}
if len(errs.(*packer.MultiError).Errors) != 2 {
t.Fatalf("Multierror should work and report 2 errors")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
var b Builder
config := testConfig()
// Test with existing dir
dir, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(dir)
config["output_directory"] = dir
b = Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["output_directory"] = "i-hope-i-dont-exist"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test with a bad value
config["shutdown_timeout"] = "this is not good"
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["shutdown_timeout"] = "5s"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHHostPort(t *testing.T) {
var b Builder
config := testConfig()
// Bad
config["host_port_min"] = 1000
config["host_port_max"] = 500
b = Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad
config["host_port_min"] = -500
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["host_port_min"] = 500
config["host_port_max"] = 1000
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHPrivateKey(t *testing.T) {
var b Builder
config := testConfig()
config["ssh_private_key_file"] = ""
b = Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["ssh_private_key_file"] = "/i/dont/exist"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad contents
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err := tf.Write([]byte("HELLO!")); err != nil {
t.Fatalf("err: %s", err)
}
config["ssh_private_key_file"] = tf.Name()
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test good contents
tf.Seek(0, 0)
tf.Truncate(0)
tf.Write([]byte(testPem))
config["ssh_private_key_file"] = tf.Name()
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test a default boot_wait
delete(config, "ssh_timeout")
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
// Test with a bad value
config["ssh_timeout"] = "this is not good"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["ssh_timeout"] = "5s"
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_QemuArgs(t *testing.T) {
var b Builder
config := testConfig()
// Test with empty
delete(config, "qemuargs")
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(b.config.QemuArgs, [][]string{}) {
t.Fatalf("bad: %#v", b.config.QemuArgs)
}
// Test with a good one
config["qemuargs"] = [][]interface{}{
{"foo", "bar", "baz"},
}
b = Builder{}
_, warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := [][]string{
{"foo", "bar", "baz"},
}
if !reflect.DeepEqual(b.config.QemuArgs, expected) {
t.Fatalf("bad: %#v", b.config.QemuArgs)
}
}
func TestBuilderPrepare_VNCPassword(t *testing.T) {
var b Builder
config := testConfig()
config["vnc_use_password"] = true
config["output_directory"] = "not-a-real-directory"
b = Builder{}
_, warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := filepath.Join("not-a-real-directory", "packer-foo.monitor")
if !reflect.DeepEqual(b.config.QMPSocketPath, expected) {
t.Fatalf("Bad QMP socket Path: %s", b.config.QMPSocketPath)
}
}
func TestCommConfigPrepare_BackwardsCompatibility(t *testing.T) {
var b Builder
config := testConfig()
hostPortMin := 1234
hostPortMax := 4321
sshTimeout := 2 * time.Minute
config["ssh_wait_timeout"] = sshTimeout
config["ssh_host_port_min"] = hostPortMin
config["ssh_host_port_max"] = hostPortMax
_, warns, err := b.Prepare(config)
if len(warns) == 0 {
t.Fatalf("should have deprecation warn")
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.CommConfig.Comm.SSHTimeout != sshTimeout {
t.Fatalf("SSHTimeout should be %s for backwards compatibility, but it was %s", sshTimeout.String(), b.config.CommConfig.Comm.SSHTimeout.String())
}
if b.config.CommConfig.HostPortMin != hostPortMin {
t.Fatalf("HostPortMin should be %d for backwards compatibility, but it was %d", hostPortMin, b.config.CommConfig.HostPortMin)
}
if b.config.CommConfig.HostPortMax != hostPortMax {
t.Fatalf("HostPortMax should be %d for backwards compatibility, but it was %d", hostPortMax, b.config.CommConfig.HostPortMax)
}
}
-619
View File
@@ -1,619 +0,0 @@
//go:generate struct-markdown
//go:generate mapstructure-to-hcl2 -type Config,QemuImgArgs
package qemu
import (
"errors"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"runtime"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/common/shutdowncommand"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
var accels = map[string]struct{}{
"none": {},
"kvm": {},
"tcg": {},
"xen": {},
"hax": {},
"hvf": {},
"whpx": {},
}
var diskInterface = map[string]bool{
"ide": true,
"scsi": true,
"virtio": true,
"virtio-scsi": true,
}
var diskCache = map[string]bool{
"writethrough": true,
"writeback": true,
"none": true,
"unsafe": true,
"directsync": true,
}
var diskDiscard = map[string]bool{
"unmap": true,
"ignore": true,
}
var diskDZeroes = map[string]bool{
"unmap": true,
"on": true,
"off": true,
}
type QemuImgArgs struct {
Convert []string `mapstructure:"convert" required:"false"`
Create []string `mapstructure:"create" required:"false"`
Resize []string `mapstructure:"resize" required:"false"`
}
type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
bootcommand.VNCConfig `mapstructure:",squash"`
shutdowncommand.ShutdownConfig `mapstructure:",squash"`
CommConfig CommConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
common.CDConfig `mapstructure:",squash"`
// Use iso from provided url. Qemu must support
// curl block device. This defaults to `false`.
ISOSkipCache bool `mapstructure:"iso_skip_cache" required:"false"`
// The accelerator type to use when running the VM.
// This may be `none`, `kvm`, `tcg`, `hax`, `hvf`, `whpx`, or `xen`. The appropriate
// software must have already been installed on your build machine to use the
// accelerator you specified. When no accelerator is specified, Packer will try
// to use `kvm` if it is available but will default to `tcg` otherwise.
//
// ~> The `hax` accelerator has issues attaching CDROM ISOs. This is an
// upstream issue which can be tracked
// [here](https://github.com/intel/haxm/issues/20).
//
// ~> The `hvf` and `whpx` accelerator are new and experimental as of
// [QEMU 2.12.0](https://wiki.qemu.org/ChangeLog/2.12#Host_support).
// You may encounter issues unrelated to Packer when using these. You may need to
// add [ "-global", "virtio-pci.disable-modern=on" ] to `qemuargs` depending on the
// guest operating system.
//
// ~> For `whpx`, note that [Stefan Weil's QEMU for Windows distribution](https://qemu.weilnetz.de/w64/)
// does not include WHPX support and users may need to compile or source a
// build of QEMU for Windows themselves with WHPX support.
Accelerator string `mapstructure:"accelerator" required:"false"`
// Additional disks to create. Uses `vm_name` as the disk name template and
// appends `-#` where `#` is the position in the array. `#` starts at 1 since 0
// is the default disk. Each string represents the disk image size in bytes.
// Optional suffixes 'k' or 'K' (kilobyte, 1024), 'M' (megabyte, 1024k), 'G'
// (gigabyte, 1024M), 'T' (terabyte, 1024G), 'P' (petabyte, 1024T) and 'E'
// (exabyte, 1024P) are supported. 'b' is ignored. Per qemu-img documentation.
// Each additional disk uses the same disk parameters as the default disk.
// Unset by default.
AdditionalDiskSize []string `mapstructure:"disk_additional_size" required:"false"`
// The number of cpus to use when building the VM.
// The default is `1` CPU.
CpuCount int `mapstructure:"cpus" required:"false"`
// The interface to use for the disk. Allowed values include any of `ide`,
// `scsi`, `virtio` or `virtio-scsi`^\*. Note also that any boot commands
// or kickstart type scripts must have proper adjustments for resulting
// device names. The Qemu builder uses `virtio` by default.
//
// ^\* Please be aware that use of the `scsi` disk interface has been
// disabled by Red Hat due to a bug described
// [here](https://bugzilla.redhat.com/show_bug.cgi?id=1019220). If you are
// running Qemu on RHEL or a RHEL variant such as CentOS, you *must* choose
// one of the other listed interfaces. Using the `scsi` interface under
// these circumstances will cause the build to fail.
DiskInterface string `mapstructure:"disk_interface" required:"false"`
// The size in bytes of the hard disk of the VM. Suffix with the first
// letter of common byte types. Use "k" or "K" for kilobytes, "M" for
// megabytes, G for gigabytes, and T for terabytes. If no value is provided
// for disk_size, Packer uses a default of `40960M` (40 GB). If a disk_size
// number is provided with no units, Packer will default to Megabytes.
DiskSize string `mapstructure:"disk_size" required:"false"`
// Packer resizes the QCOW2 image using
// qemu-img resize. Set this option to true to disable resizing.
// Defaults to false.
SkipResizeDisk bool `mapstructure:"skip_resize_disk" required:"false"`
// The cache mode to use for disk. Allowed values include any of
// `writethrough`, `writeback`, `none`, `unsafe` or `directsync`. By
// default, this is set to `writeback`.
DiskCache string `mapstructure:"disk_cache" required:"false"`
// The discard mode to use for disk. Allowed values
// include any of unmap or ignore. By default, this is set to ignore.
DiskDiscard string `mapstructure:"disk_discard" required:"false"`
// The detect-zeroes mode to use for disk.
// Allowed values include any of unmap, on or off. Defaults to off.
// When the value is "off" we don't set the flag in the qemu command, so that
// Packer still works with old versions of QEMU that don't have this option.
DetectZeroes string `mapstructure:"disk_detect_zeroes" required:"false"`
// Packer compacts the QCOW2 image using
// qemu-img convert. Set this option to true to disable compacting.
// Defaults to false.
SkipCompaction bool `mapstructure:"skip_compaction" required:"false"`
// Apply compression to the QCOW2 disk file
// using qemu-img convert. Defaults to false.
DiskCompression bool `mapstructure:"disk_compression" required:"false"`
// Either `qcow2` or `raw`, this specifies the output format of the virtual
// machine image. This defaults to `qcow2`. Due to a long-standing bug with
// `qemu-img convert` on OSX, sometimes the qemu-img convert call will
// create a corrupted image. If this is an issue for you, make sure that the
// the output format matches the input file's format, and Packer will
// perform a simple copy operation instead. See
// https://bugs.launchpad.net/qemu/+bug/1776920 for more details.
Format string `mapstructure:"format" required:"false"`
// Packer defaults to building QEMU virtual machines by
// launching a GUI that shows the console of the machine being built. When this
// value is set to `true`, the machine will start without a console.
//
// You can still see the console if you make a note of the VNC display
// number chosen, and then connect using `vncviewer -Shared <host>:<display>`
Headless bool `mapstructure:"headless" required:"false"`
// Packer defaults to building from an ISO file, this parameter controls
// whether the ISO URL supplied is actually a bootable QEMU image. When
// this value is set to `true`, the machine will either clone the source or
// use it as a backing file (if `use_backing_file` is `true`); then, it
// will resize the image according to `disk_size` and boot it.
DiskImage bool `mapstructure:"disk_image" required:"false"`
// Only applicable when disk_image is true
// and format is qcow2, set this option to true to create a new QCOW2
// file that uses the file located at iso_url as a backing file. The new file
// will only contain blocks that have changed compared to the backing file, so
// enabling this option can significantly reduce disk usage. If true, Packer
// will force the `skip_compaction` also to be true as well to skip disk
// conversion which would render the backing file feature useless.
UseBackingFile bool `mapstructure:"use_backing_file" required:"false"`
// The type of machine emulation to use. Run your qemu binary with the
// flags `-machine help` to list available types for your system. This
// defaults to `pc`.
MachineType string `mapstructure:"machine_type" required:"false"`
// The amount of memory to use when building the VM
// in megabytes. This defaults to 512 megabytes.
MemorySize int `mapstructure:"memory" required:"false"`
// The driver to use for the network interface. Allowed values `ne2k_pci`,
// `i82551`, `i82557b`, `i82559er`, `rtl8139`, `e1000`, `pcnet`, `virtio`,
// `virtio-net`, `virtio-net-pci`, `usb-net`, `i82559a`, `i82559b`,
// `i82559c`, `i82550`, `i82562`, `i82557a`, `i82557c`, `i82801`,
// `vmxnet3`, `i82558a` or `i82558b`. The Qemu builder uses `virtio-net` by
// default.
NetDevice string `mapstructure:"net_device" required:"false"`
// Connects the network to this bridge instead of using the user mode
// networking.
//
// **NB** This bridge must already exist. You can use the `virbr0` bridge
// as created by vagrant-libvirt.
//
// **NB** This will automatically enable the QMP socket (see QMPEnable).
//
// **NB** This only works in Linux based OSes.
NetBridge string `mapstructure:"net_bridge" required:"false"`
// This is the path to the directory where the
// resulting virtual machine will be created. This may be relative or absolute.
// If relative, the path is relative to the working directory when packer
// is executed. This directory must not exist or be empty prior to running
// the builder. By default this is output-BUILDNAME where "BUILDNAME" is the
// name of the build.
OutputDir string `mapstructure:"output_directory" required:"false"`
// Allows complete control over the qemu command line (though not, at this
// time, qemu-img). Each array of strings makes up a command line switch
// that overrides matching default switch/value pairs. Any value specified
// as an empty string is ignored. All values after the switch are
// concatenated with no separator.
//
// ~> **Warning:** The qemu command line allows extreme flexibility, so
// beware of conflicting arguments causing failures of your run. For
// instance, using --no-acpi could break the ability to send power signal
// type commands (e.g., shutdown -P now) to the virtual machine, thus
// preventing proper shutdown. To see the defaults, look in the packer.log
// file and search for the qemu-system-x86 command. The arguments are all
// printed for review.
//
// The following shows a sample usage:
//
// In JSON:
// ```json
// "qemuargs": [
// [ "-m", "1024M" ],
// [ "--no-acpi", "" ],
// [
// "-netdev",
// "user,id=mynet0,",
// "hostfwd=hostip:hostport-guestip:guestport",
// ""
// ],
// [ "-device", "virtio-net,netdev=mynet0" ]
// ]
// ```
//
// In HCL2:
// ```hcl
// qemuargs = [
// [ "-m", "1024M" ],
// [ "--no-acpi", "" ],
// [
// "-netdev",
// "user,id=mynet0,",
// "hostfwd=hostip:hostport-guestip:guestport",
// ""
// ],
// [ "-device", "virtio-net,netdev=mynet0" ]
// ]
// ```
//
// would produce the following (not including other defaults supplied by
// the builder and not otherwise conflicting with the qemuargs):
//
// ```text
// qemu-system-x86 -m 1024m --no-acpi -netdev
// user,id=mynet0,hostfwd=hostip:hostport-guestip:guestport -device
// virtio-net,netdev=mynet0"
// ```
//
// ~> **Windows Users:** [QEMU for Windows](https://qemu.weilnetz.de/)
// builds are available though an environmental variable does need to be
// set for QEMU for Windows to redirect stdout to the console instead of
// stdout.txt.
//
// The following shows the environment variable that needs to be set for
// Windows QEMU support:
//
// ```text
// setx SDL_STDIO_REDIRECT=0
// ```
//
// You can also use the `SSHHostPort` template variable to produce a packer
// template that can be invoked by `make` in parallel:
//
// In JSON:
// ```json
// "qemuargs": [
// [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"],
// [ "-device", "virtio-net,netdev=forward,id=net0"]
// ]
// ```
//
// In HCL2:
// ```hcl
// qemuargs = [
// [ "-netdev", "user,hostfwd=tcp::{{ .SSHHostPort }}-:22,id=forward"],
// [ "-device", "virtio-net,netdev=forward,id=net0"]
// ]
//
// `make -j 3 my-awesome-packer-templates` spawns 3 packer processes, each
// of which will bind to their own SSH port as determined by each process.
// This will also work with WinRM, just change the port forward in
// `qemuargs` to map to WinRM's default port of `5985` or whatever value
// you have the service set to listen on.
//
// This is a template engine and allows access to the following variables:
// `{{ .HTTPIP }}`, `{{ .HTTPPort }}`, `{{ .HTTPDir }}`,
// `{{ .OutputDir }}`, `{{ .Name }}`, and `{{ .SSHHostPort }}`
QemuArgs [][]string `mapstructure:"qemuargs" required:"false"`
// A map of custom arguments to pass to qemu-img commands, where the key
// is the subcommand, and the values are lists of strings for each flag.
// Example:
//
// In JSON:
// ```json
// {
// "qemu_img_args": {
// "convert": ["-o", "preallocation=full"],
// "resize": ["-foo", "bar"]
// }
// ```
// Please note
// that unlike qemuargs, these commands are not split into switch-value
// sub-arrays, because the basic elements in qemu-img calls are unlikely
// to need an actual override.
// The arguments will be constructed as follows:
// - Convert:
// Default is `qemu-img convert -O $format $sourcepath $targetpath`. Adding
// arguments ["-foo", "bar"] to qemu_img_args.convert will change this to
// `qemu-img convert -foo bar -O $format $sourcepath $targetpath`
// - Create:
// Default is `create -f $format $targetpath $size`. Adding arguments
// ["-foo", "bar"] to qemu_img_args.create will change this to
// "create -f qcow2 -foo bar target.qcow2 1234M"
// - Resize:
// Default is `qemu-img resize -f $format $sourcepath $size`. Adding
// arguments ["-foo", "bar"] to qemu_img_args.resize will change this to
// `qemu-img resize -f $format -foo bar $sourcepath $size`
QemuImgArgs QemuImgArgs `mapstructure:"qemu_img_args" required:"false"`
// The name of the Qemu binary to look for. This
// defaults to qemu-system-x86_64, but may need to be changed for
// some platforms. For example qemu-kvm, or qemu-system-i386 may be a
// better choice for some systems.
QemuBinary string `mapstructure:"qemu_binary" required:"false"`
// Enable QMP socket. Location is specified by `qmp_socket_path`. Defaults
// to false.
QMPEnable bool `mapstructure:"qmp_enable" required:"false"`
// QMP Socket Path when `qmp_enable` is true. Defaults to
// `output_directory`/`vm_name`.monitor.
QMPSocketPath string `mapstructure:"qmp_socket_path" required:"false"`
// If true, do not pass a -display option
// to qemu, allowing it to choose the default. This may be needed when running
// under macOS, and getting errors about sdl not being available.
UseDefaultDisplay bool `mapstructure:"use_default_display" required:"false"`
// What QEMU -display option to use. Defaults to gtk, use none to not pass the
// -display option allowing QEMU to choose the default. This may be needed when
// running under macOS, and getting errors about sdl not being available.
Display string `mapstructure:"display" required:"false"`
// The IP address that should be
// binded to for VNC. By default packer will use 127.0.0.1 for this. If you
// wish to bind to all interfaces use 0.0.0.0.
VNCBindAddress string `mapstructure:"vnc_bind_address" required:"false"`
// Whether or not to set a password on the VNC server. This option
// automatically enables the QMP socket. See `qmp_socket_path`. Defaults to
// `false`.
VNCUsePassword bool `mapstructure:"vnc_use_password" required:"false"`
// The minimum and maximum port
// to use for VNC access to the virtual machine. The builder uses VNC to type
// the initial boot_command. Because Packer generally runs in parallel,
// Packer uses a randomly chosen port in this range that appears available. By
// default this is 5900 to 6000. The minimum and maximum ports are inclusive.
VNCPortMin int `mapstructure:"vnc_port_min" required:"false"`
VNCPortMax int `mapstructure:"vnc_port_max"`
// This is the name of the image (QCOW2 or IMG) file for
// the new virtual machine. By default this is packer-BUILDNAME, where
// "BUILDNAME" is the name of the build. Currently, no file extension will be
// used unless it is specified in this option.
VMName string `mapstructure:"vm_name" required:"false"`
// The interface to use for the CDROM device which contains the ISO image.
// Allowed values include any of `ide`, `scsi`, `virtio` or
// `virtio-scsi`. The Qemu builder uses `virtio` by default.
// Some ARM64 images require `virtio-scsi`.
CDROMInterface string `mapstructure:"cdrom_interface" required:"false"`
// TODO(mitchellh): deprecate
RunOnce bool `mapstructure:"run_once"`
ctx interpolate.Context
}
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"boot_command",
"qemuargs",
},
},
}, raws...)
if err != nil {
return nil, err
}
// Accumulate any errors and warnings
var errs *packer.MultiError
warnings := make([]string, 0)
errs = packer.MultiErrorAppend(errs, c.ShutdownConfig.Prepare(&c.ctx)...)
if c.DiskSize == "" || c.DiskSize == "0" {
c.DiskSize = "40960M"
} else {
// Make sure supplied disk size is valid
// (digits, plus an optional valid unit character). e.g. 5000, 40G, 1t
re := regexp.MustCompile(`^[\d]+(b|k|m|g|t){0,1}$`)
matched := re.MatchString(strings.ToLower(c.DiskSize))
if !matched {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Invalid disk size."))
} else {
// Okay, it's valid -- if it doesn't alreay have a suffix, then
// append "M" as the default unit.
re = regexp.MustCompile(`^[\d]+$`)
matched = re.MatchString(strings.ToLower(c.DiskSize))
if matched {
// Needs M added.
c.DiskSize = fmt.Sprintf("%sM", c.DiskSize)
}
}
}
if c.DiskCache == "" {
c.DiskCache = "writeback"
}
if c.DiskDiscard == "" {
c.DiskDiscard = "ignore"
}
if c.DetectZeroes == "" {
c.DetectZeroes = "off"
}
if c.Accelerator == "" {
if runtime.GOOS == "windows" {
c.Accelerator = "tcg"
} else {
// /dev/kvm is a kernel module that may be loaded if kvm is
// installed and the host supports VT-x extensions. To make sure
// this will actually work we need to os.Open() it. If os.Open fails
// the kernel module was not installed or loaded correctly.
if fp, err := os.Open("/dev/kvm"); err != nil {
c.Accelerator = "tcg"
} else {
fp.Close()
c.Accelerator = "kvm"
}
}
log.Printf("use detected accelerator: %s", c.Accelerator)
} else {
log.Printf("use specified accelerator: %s", c.Accelerator)
}
if c.MachineType == "" {
c.MachineType = "pc"
}
if c.OutputDir == "" {
c.OutputDir = fmt.Sprintf("output-%s", c.PackerBuildName)
}
if c.QemuBinary == "" {
c.QemuBinary = "qemu-system-x86_64"
}
if c.MemorySize < 10 {
log.Printf("MemorySize %d is too small, using default: 512", c.MemorySize)
c.MemorySize = 512
}
if c.CpuCount < 1 {
log.Printf("CpuCount %d too small, using default: 1", c.CpuCount)
c.CpuCount = 1
}
if c.VNCBindAddress == "" {
c.VNCBindAddress = "127.0.0.1"
}
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
if c.VNCPortMax == 0 {
c.VNCPortMax = 6000
}
if c.VMName == "" {
c.VMName = fmt.Sprintf("packer-%s", c.PackerBuildName)
}
if c.Format == "" {
c.Format = "qcow2"
}
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VNCConfig.Prepare(&c.ctx)...)
if c.NetDevice == "" {
c.NetDevice = "virtio-net"
}
if c.DiskInterface == "" {
c.DiskInterface = "virtio"
}
if c.ISOSkipCache {
c.ISOChecksum = "none"
}
isoWarnings, isoErrs := c.ISOConfig.Prepare(&c.ctx)
warnings = append(warnings, isoWarnings...)
errs = packer.MultiErrorAppend(errs, isoErrs...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
commConfigWarnings, es := c.CommConfig.Prepare(&c.ctx)
if len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
warnings = append(warnings, commConfigWarnings...)
if !(c.Format == "qcow2" || c.Format == "raw") {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid format, only 'qcow2' or 'raw' are allowed"))
}
if c.Format != "qcow2" {
c.SkipCompaction = true
c.DiskCompression = false
}
if c.UseBackingFile {
c.SkipCompaction = true
if !(c.DiskImage && c.Format == "qcow2") {
errs = packer.MultiErrorAppend(
errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true"))
}
}
if c.DiskImage && len(c.AdditionalDiskSize) > 0 {
errs = packer.MultiErrorAppend(
errs, errors.New("disk_additional_size can only be used when disk_image is false"))
}
if c.SkipResizeDisk && !(c.DiskImage) {
errs = packer.MultiErrorAppend(
errs, errors.New("skip_resize_disk can only be used when disk_image is true"))
}
if _, ok := accels[c.Accelerator]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', 'whpx', or 'none' are allowed"))
}
if _, ok := diskInterface[c.DiskInterface]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk interface type"))
}
if _, ok := diskCache[c.DiskCache]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk cache type"))
}
if _, ok := diskDiscard[c.DiskDiscard]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk discard type"))
}
if _, ok := diskDZeroes[c.DetectZeroes]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("unrecognized disk detect zeroes setting"))
}
if !c.PackerForce {
if _, err := os.Stat(c.OutputDir); err == nil {
errs = packer.MultiErrorAppend(
errs,
fmt.Errorf("Output directory '%s' already exists. It must not exist.", c.OutputDir))
}
}
if c.VNCPortMin > c.VNCPortMax {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if c.NetBridge != "" && runtime.GOOS != "linux" {
errs = packer.MultiErrorAppend(
errs, fmt.Errorf("net_bridge is only supported in Linux based OSes"))
}
if c.NetBridge != "" || c.VNCUsePassword {
c.QMPEnable = true
}
if c.QMPEnable && c.QMPSocketPath == "" {
socketName := fmt.Sprintf("%s.monitor", c.VMName)
c.QMPSocketPath = filepath.Join(c.OutputDir, socketName)
}
if c.QemuArgs == nil {
c.QemuArgs = make([][]string, 0)
}
if errs != nil && len(errs.Errors) > 0 {
return warnings, errs
}
return warnings, nil
}
-694
View File
@@ -1,694 +0,0 @@
package qemu
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"testing"
"time"
"github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
)
var testPem = `
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
-----END RSA PRIVATE KEY-----
`
func testConfig() map[string]interface{} {
return map[string]interface{}{
"iso_checksum": "md5:0B0F137F17AC10944716020B018F8126",
"iso_url": "http://www.google.com/",
"ssh_username": "foo",
packer.BuildNameConfigKey: "foo",
}
}
func TestBuilderPrepare_Defaults(t *testing.T) {
var c Config
config := testConfig()
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if c.OutputDir != "output-foo" {
t.Errorf("bad output dir: %s", c.OutputDir)
}
if c.CommConfig.HostPortMin != 2222 {
t.Errorf("bad min ssh host port: %d", c.CommConfig.HostPortMin)
}
if c.CommConfig.HostPortMax != 4444 {
t.Errorf("bad max ssh host port: %d", c.CommConfig.HostPortMax)
}
if c.CommConfig.Comm.SSHPort != 22 {
t.Errorf("bad ssh port: %d", c.CommConfig.Comm.SSHPort)
}
if c.VMName != "packer-foo" {
t.Errorf("bad vm name: %s", c.VMName)
}
if c.Format != "qcow2" {
t.Errorf("bad format: %s", c.Format)
}
}
func TestBuilderPrepare_VNCBindAddress(t *testing.T) {
var c Config
config := testConfig()
// Test a default boot_wait
delete(config, "vnc_bind_address")
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if c.VNCBindAddress != "127.0.0.1" {
t.Fatalf("bad value: %s", c.VNCBindAddress)
}
}
func TestBuilderPrepare_DiskCompaction(t *testing.T) {
var c Config
config := testConfig()
// Bad
config["skip_compaction"] = false
config["disk_compression"] = true
config["format"] = "img"
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
if c.SkipCompaction != true {
t.Fatalf("SkipCompaction should be true")
}
if c.DiskCompression != false {
t.Fatalf("DiskCompression should be false")
}
// Good
config["skip_compaction"] = false
config["disk_compression"] = true
config["format"] = "qcow2"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if c.SkipCompaction != false {
t.Fatalf("SkipCompaction should be false")
}
if c.DiskCompression != true {
t.Fatalf("DiskCompression should be true")
}
}
func TestBuilderPrepare_DiskSize(t *testing.T) {
type testcase struct {
InputSize string
OutputSize string
ErrExpected bool
}
testCases := []testcase{
{"", "40960M", false}, // not provided
{"12345", "12345M", false}, // no unit given, defaults to M
{"12345x", "12345x", true}, // invalid unit
{"12345T", "12345T", false}, // terabytes
{"12345b", "12345b", false}, // bytes get preserved when set.
{"60000M", "60000M", false}, // Original test case
}
for _, tc := range testCases {
// Set input disk size
var c Config
config := testConfig()
delete(config, "disk_size")
config["disk_size"] = tc.InputSize
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if (err == nil) == tc.ErrExpected {
t.Fatalf("bad: error when providing disk size %s; Err expected: %t; err recieved: %v", tc.InputSize, tc.ErrExpected, err)
}
if c.DiskSize != tc.OutputSize {
t.Fatalf("bad size: received: %s but expected %s", c.DiskSize, tc.OutputSize)
}
}
}
func TestBuilderPrepare_AdditionalDiskSize(t *testing.T) {
var c Config
config := testConfig()
config["disk_additional_size"] = []string{"1M"}
config["disk_image"] = true
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatalf("should have error")
}
delete(config, "disk_image")
config["disk_additional_size"] = []string{"1M"}
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if c.AdditionalDiskSize[0] != "1M" {
t.Fatalf("bad size: %s", c.AdditionalDiskSize)
}
}
func TestBuilderPrepare_Format(t *testing.T) {
var c Config
config := testConfig()
// Bad
config["format"] = "illegal value"
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["format"] = "qcow2"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Good
config["format"] = "raw"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_UseBackingFile(t *testing.T) {
var c Config
config := testConfig()
config["use_backing_file"] = true
// Bad: iso_url is not a disk_image
config["disk_image"] = false
config["format"] = "qcow2"
c = Config{}
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad: format is not 'qcow2'
config["disk_image"] = true
config["format"] = "raw"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good: iso_url is a disk image and format is 'qcow2'
config["disk_image"] = true
config["format"] = "qcow2"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SkipResizeDisk(t *testing.T) {
config := testConfig()
config["skip_resize_disk"] = true
config["disk_image"] = false
var c Config
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Errorf("unexpected warns when calling prepare with skip_resize_disk set to true: %#v", warns)
}
if err == nil {
t.Errorf("setting skip_resize_disk to true when disk_image is false should have error")
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var c Config
config := testConfig()
delete(config, "floppy_files")
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("bad err: %s", err)
}
if len(c.FloppyFiles) != 0 {
t.Fatalf("bad: %#v", c.FloppyFiles)
}
floppies_path := "../../common/test-fixtures/floppies"
config["floppy_files"] = []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := []string{fmt.Sprintf("%s/bar.bat", floppies_path), fmt.Sprintf("%s/foo.ps1", floppies_path)}
if !reflect.DeepEqual(c.FloppyFiles, expected) {
t.Fatalf("bad: %#v", c.FloppyFiles)
}
}
func TestBuilderPrepare_InvalidFloppies(t *testing.T) {
var c Config
config := testConfig()
config["floppy_files"] = []string{"nonexistent.bat", "nonexistent.ps1"}
c = Config{}
_, errs := c.Prepare(config)
if errs == nil {
t.Fatalf("Nonexistent floppies should trigger multierror")
}
if len(errs.(*packer.MultiError).Errors) != 2 {
t.Fatalf("Multierror should work and report 2 errors")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var c Config
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_OutputDir(t *testing.T) {
var c Config
config := testConfig()
// Test with existing dir
dir, err := ioutil.TempDir("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.RemoveAll(dir)
config["output_directory"] = dir
c = Config{}
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["output_directory"] = "i-hope-i-dont-exist"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
var c Config
config := testConfig()
// Test with a bad value
config["shutdown_timeout"] = "this is not good"
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["shutdown_timeout"] = "5s"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHHostPort(t *testing.T) {
var c Config
config := testConfig()
// Bad
config["host_port_min"] = 1000
config["host_port_max"] = 500
c = Config{}
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad
config["host_port_min"] = -500
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["host_port_min"] = 500
config["host_port_max"] = 1000
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_SSHPrivateKey(t *testing.T) {
var c Config
config := testConfig()
config["ssh_private_key_file"] = ""
c = Config{}
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["ssh_private_key_file"] = "/i/dont/exist"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test bad contents
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
defer os.Remove(tf.Name())
defer tf.Close()
if _, err := tf.Write([]byte("HELLO!")); err != nil {
t.Fatalf("err: %s", err)
}
config["ssh_private_key_file"] = tf.Name()
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test good contents
if _, err := tf.Seek(0, 0); err != nil {
t.Fatalf("errorf getting key")
}
if err := tf.Truncate(0); err != nil {
t.Fatalf("errorf getting key")
}
if _, err := tf.Write([]byte(testPem)); err != nil {
t.Fatalf("errorf getting key")
}
config["ssh_private_key_file"] = tf.Name()
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
var c Config
config := testConfig()
// Test a default boot_wait
delete(config, "ssh_timeout")
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
// Test with a bad value
config["ssh_timeout"] = "this is not good"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Test with a good one
config["ssh_timeout"] = "5s"
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_QemuArgs(t *testing.T) {
var c Config
config := testConfig()
// Test with empty
delete(config, "qemuargs")
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(c.QemuArgs, [][]string{}) {
t.Fatalf("bad: %#v", c.QemuArgs)
}
// Test with a good one
config["qemuargs"] = [][]interface{}{
{"foo", "bar", "baz"},
}
c = Config{}
warns, err = c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := [][]string{
{"foo", "bar", "baz"},
}
if !reflect.DeepEqual(c.QemuArgs, expected) {
t.Fatalf("bad: %#v", c.QemuArgs)
}
}
func TestBuilderPrepare_VNCPassword(t *testing.T) {
var c Config
config := testConfig()
config["vnc_use_password"] = true
config["output_directory"] = "not-a-real-directory"
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
expected := filepath.Join("not-a-real-directory", "packer-foo.monitor")
if !reflect.DeepEqual(c.QMPSocketPath, expected) {
t.Fatalf("Bad QMP socket Path: %s", c.QMPSocketPath)
}
}
func TestCommConfigPrepare_BackwardsCompatibility(t *testing.T) {
var c Config
config := testConfig()
hostPortMin := 1234
hostPortMax := 4321
sshTimeout := 2 * time.Minute
config["ssh_wait_timeout"] = sshTimeout
config["ssh_host_port_min"] = hostPortMin
config["ssh_host_port_max"] = hostPortMax
warns, err := c.Prepare(config)
if len(warns) == 0 {
t.Fatalf("should have deprecation warn")
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if c.CommConfig.Comm.SSHTimeout != sshTimeout {
t.Fatalf("SSHTimeout should be %s for backwards compatibility, but it was %s", sshTimeout.String(), c.CommConfig.Comm.SSHTimeout.String())
}
if c.CommConfig.HostPortMin != hostPortMin {
t.Fatalf("HostPortMin should be %d for backwards compatibility, but it was %d", hostPortMin, c.CommConfig.HostPortMin)
}
if c.CommConfig.HostPortMax != hostPortMax {
t.Fatalf("HostPortMax should be %d for backwards compatibility, but it was %d", hostPortMax, c.CommConfig.HostPortMax)
}
}
func TestBuilderPrepare_LoadQemuImgArgs(t *testing.T) {
var c Config
config := testConfig()
config["qemu_img_args"] = map[string][]string{
"convert": []string{"-o", "preallocation=full"},
"resize": []string{"-foo", "bar"},
"create": []string{"-baz", "bang"},
}
warns, err := c.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
assert.Equal(t, []string{"-o", "preallocation=full"},
c.QemuImgArgs.Convert, "Convert args not loaded properly")
assert.Equal(t, []string{"-foo", "bar"},
c.QemuImgArgs.Resize, "Resize args not loaded properly")
assert.Equal(t, []string{"-baz", "bang"},
c.QemuImgArgs.Create, "Create args not loaded properly")
}
-32
View File
@@ -6,7 +6,6 @@ import (
"fmt"
"io"
"log"
"os"
"os/exec"
"regexp"
"strings"
@@ -23,10 +22,6 @@ type DriverCancelCallback func(state multistep.StateBag) bool
// A driver is able to talk to qemu-system-x86_64 and perform certain
// operations with it.
type Driver interface {
// Copy bypasses qemu-img convert and directly copies an image
// that doesn't need converting.
Copy(string, string) error
// Stop stops a running machine, forcefully.
Stop() error
@@ -70,33 +65,6 @@ func (d *QemuDriver) Stop() error {
return nil
}
func (d *QemuDriver) Copy(sourceName, targetName string) error {
source, err := os.Open(sourceName)
if err != nil {
err = fmt.Errorf("Error opening iso for copy: %s", err)
return err
}
defer source.Close()
// Create will truncate an existing file
target, err := os.Create(targetName)
if err != nil {
err = fmt.Errorf("Error creating hard drive in output dir: %s", err)
return err
}
defer target.Close()
log.Printf("Copying %s to %s", source.Name(), target.Name())
bytes, err := io.Copy(target, source)
if err != nil {
err = fmt.Errorf("Error copying iso to output dir: %s", err)
return err
}
log.Printf(fmt.Sprintf("Copied %d bytes", bytes))
return nil
}
func (d *QemuDriver) Qemu(qemuArgs ...string) error {
d.lock.Lock()
defer d.lock.Unlock()
+3 -13
View File
@@ -5,9 +5,6 @@ import "sync"
type DriverMock struct {
sync.Mutex
CopyCalled bool
CopyErr error
StopCalled bool
StopErr error
@@ -17,9 +14,8 @@ type DriverMock struct {
WaitForShutdownCalled bool
WaitForShutdownState bool
QemuImgCalled bool
QemuImgCalls []string
QemuImgErrs []error
QemuImgCalls [][]string
QemuImgErrs []error
VerifyCalled bool
VerifyErr error
@@ -29,11 +25,6 @@ type DriverMock struct {
VersionErr error
}
func (d *DriverMock) Copy(source, dst string) error {
d.CopyCalled = true
return d.CopyErr
}
func (d *DriverMock) Stop() error {
d.StopCalled = true
return d.StopErr
@@ -54,8 +45,7 @@ func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool {
}
func (d *DriverMock) QemuImg(args ...string) error {
d.QemuImgCalled = true
d.QemuImgCalls = append(d.QemuImgCalls, args...)
d.QemuImgCalls = append(d.QemuImgCalls, args)
if len(d.QemuImgErrs) >= len(d.QemuImgCalls) {
return d.QemuImgErrs[len(d.QemuImgCalls)-1]
+20 -31
View File
@@ -17,32 +17,37 @@ import (
// This step converts the virtual disk that was used as the
// hard drive for the virtual machine.
type stepConvertDisk struct {
DiskCompression bool
Format string
OutputDir string
SkipCompaction bool
VMName string
QemuImgArgs QemuImgArgs
}
type stepConvertDisk struct{}
func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
diskName := config.VMName
ui := state.Get("ui").(packer.Ui)
diskName := s.VMName
if s.SkipCompaction && !s.DiskCompression {
if config.SkipCompaction && !config.DiskCompression {
return multistep.ActionContinue
}
name := diskName + ".convert"
sourcePath := filepath.Join(s.OutputDir, diskName)
targetPath := filepath.Join(s.OutputDir, name)
sourcePath := filepath.Join(config.OutputDir, diskName)
targetPath := filepath.Join(config.OutputDir, name)
command := s.buildConvertCommand(sourcePath, targetPath)
command := []string{
"convert",
}
if config.DiskCompression {
command = append(command, "-c")
}
command = append(command, []string{
"-O", config.Format,
sourcePath,
targetPath,
}...,
)
ui.Say("Converting hard drive...")
// Retry the conversion a few times in case it takes the qemu process a
@@ -85,20 +90,4 @@ func (s *stepConvertDisk) Run(ctx context.Context, state multistep.StateBag) mul
return multistep.ActionContinue
}
func (s *stepConvertDisk) buildConvertCommand(sourcePath, targetPath string) []string {
command := []string{"convert"}
if s.DiskCompression {
command = append(command, "-c")
}
// Add user-provided convert args
command = append(command, s.QemuImgArgs.Convert...)
// Add format, and paths.
command = append(command, "-O", s.Format, sourcePath, targetPath)
return command
}
func (s *stepConvertDisk) Cleanup(state multistep.StateBag) {}
-52
View File
@@ -1,52 +0,0 @@
package qemu
import (
"fmt"
"testing"
"github.com/stretchr/testify/assert"
)
func Test_buildConvertCommand(t *testing.T) {
type testCase struct {
Step *stepConvertDisk
Expected []string
Reason string
}
testcases := []testCase{
{
&stepConvertDisk{
Format: "qcow2",
DiskCompression: false,
},
[]string{"convert", "-O", "qcow2", "source.qcow", "target.qcow2"},
"Basic, happy path, no compression, no extra args",
},
{
&stepConvertDisk{
Format: "qcow2",
DiskCompression: true,
},
[]string{"convert", "-c", "-O", "qcow2", "source.qcow", "target.qcow2"},
"Basic, happy path, with compression, no extra args",
},
{
&stepConvertDisk{
Format: "qcow2",
DiskCompression: true,
QemuImgArgs: QemuImgArgs{
Convert: []string{"-o", "preallocation=full"},
},
},
[]string{"convert", "-c", "-o", "preallocation=full", "-O", "qcow2", "source.qcow", "target.qcow2"},
"Basic, happy path, with compression, one set of extra args",
},
}
for _, tc := range testcases {
command := tc.Step.buildConvertCommand("source.qcow", "target.qcow2")
assert.Equal(t, command, tc.Expected,
fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected))
}
}
+9 -37
View File
@@ -11,42 +11,26 @@ import (
// This step copies the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepCopyDisk struct {
DiskImage bool
Format string
OutputDir string
UseBackingFile bool
VMName string
QemuImgArgs QemuImgArgs
}
type stepCopyDisk struct{}
func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
isoPath := state.Get("iso_path").(string)
ui := state.Get("ui").(packer.Ui)
path := filepath.Join(s.OutputDir, s.VMName)
path := filepath.Join(config.OutputDir, config.VMName)
if !s.DiskImage || s.UseBackingFile {
return multistep.ActionContinue
command := []string{
"convert",
"-O", config.Format,
isoPath,
path,
}
// isoPath extention is:
ext := filepath.Ext(isoPath)
if ext[1:] == s.Format {
ui.Message("File extension already matches desired output format. " +
"Skipping qemu-img convert step")
err := driver.Copy(isoPath, path)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if !config.DiskImage || config.UseBackingFile {
return multistep.ActionContinue
}
command := s.buildConvertCommand(isoPath, path)
ui.Say("Copying hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
@@ -58,16 +42,4 @@ func (s *stepCopyDisk) Run(ctx context.Context, state multistep.StateBag) multis
return multistep.ActionContinue
}
func (s *stepCopyDisk) buildConvertCommand(sourcePath, targetPath string) []string {
command := []string{"convert"}
// Add user-provided convert args
command = append(command, s.QemuImgArgs.Convert...)
// Add format, and paths.
command = append(command, "-O", s.Format, sourcePath, targetPath)
return command
}
func (s *stepCopyDisk) Cleanup(state multistep.StateBag) {}
-122
View File
@@ -1,122 +0,0 @@
package qemu
import (
"context"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
)
func copyTestState(t *testing.T, d *DriverMock) multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("ui", packer.TestUi(t))
state.Put("driver", d)
state.Put("iso_path", "example_source.qcow2")
return state
}
func Test_StepCopySkip(t *testing.T) {
testcases := []stepCopyDisk{
stepCopyDisk{
DiskImage: false,
UseBackingFile: false,
},
stepCopyDisk{
DiskImage: true,
UseBackingFile: true,
},
stepCopyDisk{
DiskImage: false,
UseBackingFile: true,
},
}
for _, tc := range testcases {
d := new(DriverMock)
state := copyTestState(t, d)
action := tc.Run(context.TODO(), state)
if action != multistep.ActionContinue {
t.Fatalf("Should have gotten an ActionContinue")
}
if d.CopyCalled || d.QemuImgCalled {
t.Fatalf("Should have skipped step since DiskImage and UseBackingFile are not set")
}
}
}
func Test_StepCopyCalled(t *testing.T) {
step := stepCopyDisk{
DiskImage: true,
Format: "qcow2",
VMName: "output.qcow2",
}
d := new(DriverMock)
state := copyTestState(t, d)
action := step.Run(context.TODO(), state)
if action != multistep.ActionContinue {
t.Fatalf("Should have gotten an ActionContinue")
}
if !d.CopyCalled {
t.Fatalf("Should have copied since all extensions are qcow2")
}
if d.QemuImgCalled {
t.Fatalf("Should not have called qemu-img when formats match")
}
}
func Test_StepQemuImgCalled(t *testing.T) {
step := stepCopyDisk{
DiskImage: true,
Format: "raw",
VMName: "output.qcow2",
}
d := new(DriverMock)
state := copyTestState(t, d)
action := step.Run(context.TODO(), state)
if action != multistep.ActionContinue {
t.Fatalf("Should have gotten an ActionContinue")
}
if d.CopyCalled {
t.Fatalf("Should not have copied since extensions don't match")
}
if !d.QemuImgCalled {
t.Fatalf("Should have called qemu-img since extensions don't match")
}
}
func Test_StepQemuImgCalledWithExtraArgs(t *testing.T) {
step := &stepCopyDisk{
DiskImage: true,
Format: "raw",
VMName: "output.qcow2",
QemuImgArgs: QemuImgArgs{
Convert: []string{"-o", "preallocation=full"},
},
}
d := new(DriverMock)
state := copyTestState(t, d)
action := step.Run(context.TODO(), state)
if action != multistep.ActionContinue {
t.Fatalf("Should have gotten an ActionContinue")
}
if d.CopyCalled {
t.Fatalf("Should not have copied since extensions don't match")
}
if !d.QemuImgCalled {
t.Fatalf("Should have called qemu-img since extensions don't match")
}
assert.Equal(
t,
d.QemuImgCalls,
[]string{"convert", "-o", "preallocation=full", "-O", "raw",
"example_source.qcow2", "output.qcow2"},
"should have added user extra args")
}
+21 -35
View File
@@ -12,23 +12,15 @@ import (
// This step creates the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepCreateDisk struct {
AdditionalDiskSize []string
DiskImage bool
DiskSize string
Format string
OutputDir string
UseBackingFile bool
VMName string
QemuImgArgs QemuImgArgs
}
type stepCreateDisk struct{}
func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
name := s.VMName
name := config.VMName
if s.DiskImage && !s.UseBackingFile {
if config.DiskImage && !config.UseBackingFile {
return multistep.ActionContinue
}
@@ -36,12 +28,12 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult
ui.Say("Creating required virtual machine disks")
// The 'main' or 'default' disk
diskFullPaths = append(diskFullPaths, filepath.Join(s.OutputDir, name))
diskSizes = append(diskSizes, s.DiskSize)
diskFullPaths = append(diskFullPaths, filepath.Join(config.OutputDir, name))
diskSizes = append(diskSizes, config.DiskSize)
// Additional disks
if len(s.AdditionalDiskSize) > 0 {
for i, diskSize := range s.AdditionalDiskSize {
path := filepath.Join(s.OutputDir, fmt.Sprintf("%s-%d", name, i+1))
if len(config.AdditionalDiskSize) > 0 {
for i, diskSize := range config.AdditionalDiskSize {
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d", name, i+1))
diskFullPaths = append(diskFullPaths, path)
size := diskSize
diskSizes = append(diskSizes, size)
@@ -51,8 +43,19 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult
// Create all required disks
for i, diskFullPath := range diskFullPaths {
log.Printf("[INFO] Creating disk with Path: %s and Size: %s", diskFullPath, diskSizes[i])
command := []string{
"create",
"-f", config.Format,
}
command := s.buildCreateCommand(diskFullPath, diskSizes[i], i, state)
if config.UseBackingFile && i == 0 {
isoPath := state.Get("iso_path").(string)
command = append(command, "-b", isoPath)
}
command = append(command,
diskFullPath,
diskSizes[i])
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error creating hard drive: %s", err)
@@ -68,21 +71,4 @@ func (s *stepCreateDisk) Run(ctx context.Context, state multistep.StateBag) mult
return multistep.ActionContinue
}
func (s *stepCreateDisk) buildCreateCommand(path string, size string, i int, state multistep.StateBag) []string {
command := []string{"create", "-f", s.Format}
if s.UseBackingFile && i == 0 {
isoPath := state.Get("iso_path").(string)
command = append(command, "-b", isoPath)
}
// add user-provided convert args
command = append(command, s.QemuImgArgs.Create...)
// add target path and size.
command = append(command, path, size)
return command
}
func (s *stepCreateDisk) Cleanup(state multistep.StateBag) {}
-80
View File
@@ -1,80 +0,0 @@
package qemu
import (
"fmt"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/stretchr/testify/assert"
)
func Test_buildCreateCommand(t *testing.T) {
type testCase struct {
Step *stepCreateDisk
I int
Expected []string
Reason string
}
testcases := []testCase{
{
&stepCreateDisk{
Format: "qcow2",
UseBackingFile: false,
},
0,
[]string{"create", "-f", "qcow2", "target.qcow2", "1234M"},
"Basic, happy path, no backing store, no extra args",
},
{
&stepCreateDisk{
Format: "qcow2",
UseBackingFile: true,
},
0,
[]string{"create", "-f", "qcow2", "-b", "source.qcow2", "target.qcow2", "1234M"},
"Basic, happy path, backing store, no extra args",
},
{
&stepCreateDisk{
Format: "qcow2",
UseBackingFile: true,
},
1,
[]string{"create", "-f", "qcow2", "target.qcow2", "1234M"},
"Basic, happy path, backing store set but not at first index, no extra args",
},
{
&stepCreateDisk{
Format: "qcow2",
UseBackingFile: true,
QemuImgArgs: QemuImgArgs{
Create: []string{"-foo", "bar"},
},
},
0,
[]string{"create", "-f", "qcow2", "-b", "source.qcow2", "-foo", "bar", "target.qcow2", "1234M"},
"Basic, happy path, backing store set, extra args",
},
{
&stepCreateDisk{
Format: "qcow2",
UseBackingFile: true,
QemuImgArgs: QemuImgArgs{
Create: []string{"-foo", "bar"},
},
},
1,
[]string{"create", "-f", "qcow2", "-foo", "bar", "target.qcow2", "1234M"},
"Basic, happy path, backing store set but not at first index, extra args",
},
}
for _, tc := range testcases {
state := new(multistep.BasicStateBag)
state.Put("iso_path", "source.qcow2")
command := tc.Step.buildCreateCommand("target.qcow2", "1234M", tc.I, state)
assert.Equal(t, command, tc.Expected,
fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected))
}
}
-12
View File
@@ -13,9 +13,6 @@ import (
// This step adds a NAT port forwarding definition so that SSH or WinRM is available
// on the guest machine.
type stepPortForward struct {
CommunicatorType string
NetBridge string
l *net.Listener
}
@@ -23,15 +20,6 @@ func (s *stepPortForward) Run(ctx context.Context, state multistep.StateBag) mul
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
if s.CommunicatorType == "none" {
ui.Message("No communicator is set; skipping port forwarding setup.")
return multistep.ActionContinue
}
if s.NetBridge != "" {
ui.Message("net_bridge is set; skipping port forwarding setup.")
return multistep.ActionContinue
}
commHostPort := config.CommConfig.Comm.Port()
if config.CommConfig.SkipNatMapping {
+10 -26
View File
@@ -11,26 +11,22 @@ import (
// This step resizes the virtual disk that will be used as the
// hard drive for the virtual machine.
type stepResizeDisk struct {
DiskCompression bool
DiskImage bool
Format string
OutputDir string
SkipResizeDisk bool
VMName string
DiskSize string
QemuImgArgs QemuImgArgs
}
type stepResizeDisk struct{}
func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
path := filepath.Join(s.OutputDir, s.VMName)
path := filepath.Join(config.OutputDir, config.VMName)
command := s.buildResizeCommand(path)
command := []string{
"resize",
"-f", config.Format,
path,
config.DiskSize,
}
if s.DiskImage == false || s.SkipResizeDisk == true {
if config.DiskImage == false || config.SkipResizeDisk == true {
return multistep.ActionContinue
}
@@ -45,16 +41,4 @@ func (s *stepResizeDisk) Run(ctx context.Context, state multistep.StateBag) mult
return multistep.ActionContinue
}
func (s *stepResizeDisk) buildResizeCommand(path string) []string {
command := []string{"resize", "-f", s.Format}
// add user-provided convert args
command = append(command, s.QemuImgArgs.Resize...)
// Add file and size
command = append(command, path, s.DiskSize)
return command
}
func (s *stepResizeDisk) Cleanup(state multistep.StateBag) {}
+64 -59
View File
@@ -2,76 +2,81 @@ package qemu
import (
"context"
"fmt"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/stretchr/testify/assert"
)
func TestStepResizeDisk_Skips(t *testing.T) {
testConfigs := []*Config{
&Config{
DiskImage: false,
SkipResizeDisk: false,
},
&Config{
DiskImage: false,
SkipResizeDisk: true,
},
func TestStepResizeDisk_Run(t *testing.T) {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{
DiskImage: true,
SkipResizeDisk: false,
DiskSize: "4096M",
Format: "qcow2",
OutputDir: "/test/",
VMName: "test",
}
for _, config := range testConfigs {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
state.Put("config", config)
step := new(stepResizeDisk)
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) > 0 {
t.Fatal("should NOT have called qemu-img")
}
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) == 0 {
t.Fatal("should qemu-img called")
}
if len(driver.QemuImgCalls[0]) != 5 {
t.Fatal("should 5 qemu-img parameters")
}
}
func Test_buildResizeCommand(t *testing.T) {
type testCase struct {
Step *stepResizeDisk
Expected []string
Reason string
func TestStepResizeDisk_SkipIso(t *testing.T) {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{
DiskImage: false,
SkipResizeDisk: false,
}
testcases := []testCase{
{
&stepResizeDisk{
Format: "qcow2",
DiskSize: "1234M",
},
[]string{"resize", "-f", "qcow2", "source.qcow", "1234M"},
"no extra args",
},
{
&stepResizeDisk{
Format: "qcow2",
DiskSize: "1234M",
QemuImgArgs: QemuImgArgs{
Resize: []string{"-foo", "bar"},
},
},
[]string{"resize", "-f", "qcow2", "-foo", "bar", "source.qcow", "1234M"},
"one set of extra args",
},
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
for _, tc := range testcases {
command := tc.Step.buildResizeCommand("source.qcow")
assert.Equal(t, command, tc.Expected,
fmt.Sprintf("%s. Expected %#v", tc.Reason, tc.Expected))
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) > 0 {
t.Fatal("should NOT qemu-img called")
}
}
func TestStepResizeDisk_SkipOption(t *testing.T) {
state := testState(t)
driver := state.Get("driver").(*DriverMock)
config := &Config{
DiskImage: false,
SkipResizeDisk: true,
}
state.Put("config", config)
step := new(stepResizeDisk)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
if len(driver.QemuImgCalls) > 0 {
t.Fatal("should NOT qemu-img called")
}
}
+9 -28
View File
@@ -15,7 +15,8 @@ import (
// stepRun runs the virtual machine
type stepRun struct {
DiskImage bool
BootDrive string
Message string
}
type qemuArgsTemplateData struct {
@@ -31,17 +32,9 @@ func (s *stepRun) Run(ctx context.Context, state multistep.StateBag) multistep.S
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
// Run command is different depending whether we're booting from an
// installation CD or a pre-baked image
bootDrive := "once=d"
message := "Starting VM, booting from CD-ROM"
if s.DiskImage {
bootDrive = "c"
message = "Starting VM, booting disk image"
}
ui.Say(message)
ui.Say(s.Message)
command, err := getCommandArgs(bootDrive, state)
command, err := getCommandArgs(s.BootDrive, state)
if err != nil {
err := fmt.Errorf("Error processing QemuArgs: %s", err)
ui.Error(err.Error())
@@ -197,26 +190,14 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
}
}
cdPaths := []string{}
// Add the installation CD to the run command
if !config.DiskImage {
cdPaths = append(cdPaths, isoPath)
}
// Add our custom CD created from cd_files, if it exists
cdFilesPath, ok := state.Get("cd_path").(string)
if ok {
if cdFilesPath != "" {
cdPaths = append(cdPaths, cdFilesPath)
}
}
for i, cdPath := range cdPaths {
if config.CDROMInterface == "" {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,index=%d,media=cdrom", cdPath, i))
defaultArgs["-cdrom"] = isoPath
} else if config.CDROMInterface == "virtio-scsi" {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=none,index=%d,id=cdrom%d,media=cdrom", cdPath, i, i))
deviceArgs = append(deviceArgs, "virtio-scsi-device", fmt.Sprintf("scsi-cd,drive=cdrom%d", i))
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=none,id=cdrom,media=cdrom", isoPath))
deviceArgs = append(deviceArgs, "virtio-scsi-device", "scsi-cd,drive=cdrom")
} else {
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,index=%d,id=cdrom%d,media=cdrom", cdPath, config.CDROMInterface, i, i))
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,id=cdrom,media=cdrom", isoPath, config.CDROMInterface))
}
}
@@ -247,7 +228,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
inArgs := make(map[string][]string)
if len(config.QemuArgs) > 0 {
ui.Say("Overriding default Qemu arguments with QemuArgs...")
ui.Say("Overriding defaults Qemu arguments with QemuArgs...")
httpIp := state.Get("http_ip").(string)
httpPort := state.Get("http_port").(int)
-148
View File
@@ -1,148 +0,0 @@
package qemu
import (
"fmt"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/stretchr/testify/assert"
)
func runTestState(t *testing.T, config *Config) multistep.StateBag {
state := new(multistep.BasicStateBag)
state.Put("ui", packer.TestUi(t))
state.Put("config", config)
d := new(DriverMock)
d.VersionResult = "3.0.0"
state.Put("driver", d)
state.Put("commHostPort", 5000)
state.Put("floppy_path", "fake_floppy_path")
state.Put("http_ip", "127.0.0.1")
state.Put("http_port", 1234)
state.Put("iso_path", "/path/to/test.iso")
state.Put("qemu_disk_paths", []string{})
state.Put("vnc_port", 5905)
state.Put("vnc_password", "fake_vnc_password")
return state
}
func Test_getCommandArgs(t *testing.T) {
state := runTestState(t, &Config{})
args, err := getCommandArgs("", state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
expected := []string{
"-display", "gtk",
"-m", "0M",
"-boot", "",
"-fda", "fake_floppy_path",
"-name", "",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905",
"-machine", "type=,accel=",
"-device", ",netdev=user.0",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
}
assert.ElementsMatch(t, args, expected, "unexpected generated args")
}
func Test_CDFilesPath(t *testing.T) {
// cd_path is set and DiskImage is false
state := runTestState(t, &Config{})
state.Put("cd_path", "fake_cd_path.iso")
args, err := getCommandArgs("", state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
expected := []string{
"-display", "gtk",
"-m", "0M",
"-boot", "",
"-fda", "fake_floppy_path",
"-name", "",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905",
"-machine", "type=,accel=",
"-device", ",netdev=user.0",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-drive", "file=fake_cd_path.iso,index=1,media=cdrom",
}
assert.ElementsMatch(t, args, expected, fmt.Sprintf("unexpected generated args: %#v", args))
// cd_path is set and DiskImage is true
config := &Config{
DiskImage: true,
DiskInterface: "virtio-scsi",
}
state = runTestState(t, config)
state.Put("cd_path", "fake_cd_path.iso")
args, err = getCommandArgs("c", state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
expected = []string{
"-display", "gtk",
"-m", "0M",
"-boot", "c",
"-fda", "fake_floppy_path",
"-name", "",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905",
"-machine", "type=,accel=",
"-device", ",netdev=user.0",
"-device", "virtio-scsi-pci,id=scsi0",
"-device", "scsi-hd,bus=scsi0.0,drive=drive0",
"-drive", "if=none,file=,id=drive0,cache=,discard=,format=,detect-zeroes=",
"-drive", "file=fake_cd_path.iso,index=0,media=cdrom",
}
assert.ElementsMatch(t, args, expected, fmt.Sprintf("unexpected generated args: %#v", args))
}
func Test_OptionalConfigOptionsGetSet(t *testing.T) {
c := &Config{
VNCUsePassword: true,
QMPEnable: true,
QMPSocketPath: "qmp_path",
VMName: "MyFancyName",
MachineType: "pc",
Accelerator: "hvf",
}
state := runTestState(t, c)
args, err := getCommandArgs("once=d", state)
if err != nil {
t.Fatalf("should not have an error getting args. Error: %s", err)
}
expected := []string{
"-display", "gtk",
"-m", "0M",
"-boot", "once=d",
"-fda", "fake_floppy_path",
"-name", "MyFancyName",
"-netdev", "user,id=user.0,hostfwd=tcp::5000-:0",
"-vnc", ":5905,password",
"-machine", "type=pc,accel=hvf",
"-device", ",netdev=user.0",
"-drive", "file=/path/to/test.iso,index=0,media=cdrom",
"-qmp", "unix:qmp_path,server,nowait",
}
assert.ElementsMatch(t, args, expected, "password flag should be set, and d drive should be set.")
}
+3 -15
View File
@@ -19,31 +19,19 @@ import (
// This step waits for the guest address to become available in the network
// bridge, then it sets the guestAddress state property.
type stepWaitGuestAddress struct {
CommunicatorType string
NetBridge string
timeout time.Duration
}
func (s *stepWaitGuestAddress) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ui := state.Get("ui").(packer.Ui)
if s.CommunicatorType == "none" {
ui.Message("No communicator is configured -- skipping StepWaitGuestAddress")
return multistep.ActionContinue
}
if s.NetBridge == "" {
ui.Message("Not using a NetBridge -- skipping StepWaitGuestAddress")
return multistep.ActionContinue
}
qmpMonitor := state.Get("qmp_monitor").(*qmp.SocketMonitor)
ctx, cancel := context.WithTimeout(ctx, s.timeout)
defer cancel()
ui.Say(fmt.Sprintf("Waiting for the guest address to become available in the %s network bridge...", s.NetBridge))
ui.Say(fmt.Sprintf("Waiting for the guest address to become available in the %s network bridge...", config.NetBridge))
for {
guestAddress := getGuestAddress(qmpMonitor, s.NetBridge, "user.0")
guestAddress := getGuestAddress(qmpMonitor, config.NetBridge, "user.0")
if guestAddress != "" {
log.Printf("Found guest address %s", guestAddress)
state.Put("guestAddress", guestAddress)
-5
View File
@@ -65,11 +65,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
state.Put("ui", ui)
steps := []multistep.Step{
&stepPreValidate{
Force: b.config.PackerForce,
ImageName: b.config.ImageName,
SnapshotName: b.config.SnapshotName,
},
&stepCreateSSHKey{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("scw_%s.pem", b.config.PackerBuildName),
-80
View File
@@ -1,80 +0,0 @@
package scaleway
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
// StepPreValidate provides an opportunity to pre-validate any configuration for
// the build before actually doing any time consuming work
//
type stepPreValidate struct {
Force bool
ImageName string
SnapshotName string
}
func (s *stepPreValidate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if s.Force {
ui.Say("Force flag found, skipping prevalidating image name")
return multistep.ActionContinue
}
ui.Say(fmt.Sprintf("Prevalidating image name: %s", s.ImageName))
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
images, err := instanceAPI.ListImages(
&instance.ListImagesRequest{Name: &s.ImageName},
scw.WithAllPages())
if err != nil {
err := fmt.Errorf("Error: getting image list: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for _, im := range images.Images {
if im.Name == s.ImageName {
err := fmt.Errorf("Error: image name: '%s' is used by existing image with ID %s",
s.ImageName, im.ID)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
ui.Say(fmt.Sprintf("Prevalidating snapshot name: %s", s.SnapshotName))
snapshots, err := instanceAPI.ListSnapshots(
&instance.ListSnapshotsRequest{Name: &s.SnapshotName},
scw.WithAllPages())
if err != nil {
err := fmt.Errorf("Error: getting snapshot list: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
for _, sn := range snapshots.Snapshots {
if sn.Name == s.SnapshotName {
err := fmt.Errorf("Error: snapshot name: '%s' is used by existing snapshot with ID %s",
s.SnapshotName, sn.ID)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}
func (s *stepPreValidate) Cleanup(multistep.StateBag) {
}
-154
View File
@@ -1,154 +0,0 @@
package scaleway
import (
"bytes"
"context"
"encoding/json"
"math/rand"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
"github.com/scaleway/scaleway-sdk-go/scw"
)
// 1. Configure a httptest server to return the list of fakeImgNames or fakeSnapNames
// (depending on the endpoint).
// 2. Instantiate a Scaleway API client and wire it to send requests to the httptest
// server.
// 3. Return a state (containing the client) ready to be passed to the step.Run() method.
// 4. Return a teardown function meant to be deferred from the test.
func setup(t *testing.T, fakeImgNames []string, fakeSnapNames []string) (*multistep.BasicStateBag, func()) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
enc := json.NewEncoder(w)
switch r.URL.Path {
case "/instance/v1/zones/fr-par-1/images":
var imgs instance.ListImagesResponse
for _, name := range fakeImgNames {
imgs.Images = append(imgs.Images, &instance.Image{
ID: strconv.Itoa(rand.Int()),
Name: name,
Zone: "fr-par-1",
})
}
imgs.TotalCount = uint32(len(fakeImgNames))
if err := enc.Encode(imgs); err != nil {
t.Fatalf("fake server: encoding reply: %s", err)
}
case "/instance/v1/zones/fr-par-1/snapshots":
var snaps instance.ListSnapshotsResponse
for _, name := range fakeSnapNames {
snaps.Snapshots = append(snaps.Snapshots, &instance.Snapshot{
ID: strconv.Itoa(rand.Int()),
Name: name,
Zone: "fr-par-1",
})
}
snaps.TotalCount = uint32(len(fakeSnapNames))
if err := enc.Encode(snaps); err != nil {
t.Fatalf("fake server: encoding reply: %s", err)
}
default:
t.Fatalf("fake server: unexpected path: %q", r.URL.Path)
}
}))
clientOpts := []scw.ClientOption{
scw.WithDefaultZone(scw.ZoneFrPar1),
scw.WithAPIURL(ts.URL),
}
client, err := scw.NewClient(clientOpts...)
if err != nil {
ts.Close()
t.Fatalf("setup: client: %s", err)
}
state := multistep.BasicStateBag{}
state.Put("ui", &packer.BasicUi{
Reader: new(bytes.Buffer),
Writer: new(bytes.Buffer),
})
state.Put("client", client)
teardown := func() {
ts.Close()
}
return &state, teardown
}
func TestStepPreValidate(t *testing.T) {
testCases := []struct {
name string
fakeImgNames []string
fakeSnapNames []string
step stepPreValidate
wantAction multistep.StepAction
}{
{"happy path: both image name and snapshot name are new",
[]string{"image-old"},
[]string{"snapshot-old"},
stepPreValidate{
Force: false,
ImageName: "image-new",
SnapshotName: "snapshot-new",
},
multistep.ActionContinue,
},
{"want failure: old image name",
[]string{"image-old"},
[]string{"snapshot-old"},
stepPreValidate{
Force: false,
ImageName: "image-old",
SnapshotName: "snapshot-new",
},
multistep.ActionHalt,
},
{"want failure: old snapshot name",
[]string{"image-old"},
[]string{"snapshot-old"},
stepPreValidate{
Force: false,
ImageName: "image-new",
SnapshotName: "snapshot-old",
},
multistep.ActionHalt,
},
{"old image name but force flag",
[]string{"image-old"},
[]string{"snapshot-old"},
stepPreValidate{
Force: true,
ImageName: "image-old",
SnapshotName: "snapshot-new",
},
multistep.ActionContinue,
},
{"old snapshot name but force flag",
[]string{"image-old"},
[]string{"snapshot-old"},
stepPreValidate{
Force: true,
ImageName: "image-new",
SnapshotName: "snapshot-old",
},
multistep.ActionContinue,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
state, teardown := setup(t, tc.fakeImgNames, tc.fakeSnapNames)
defer teardown()
if action := tc.step.Run(context.Background(), state); action != tc.wantAction {
t.Fatalf("step.Run: want: %v; got: %v", tc.wantAction, action)
}
})
}
}
@@ -4,7 +4,6 @@ package common
import (
"fmt"
"strings"
"github.com/hashicorp/packer/template/interpolate"
)
@@ -20,36 +19,13 @@ const (
type GuestAdditionsConfig struct {
Communicator string `mapstructure:"communicator"`
// The method by which guest additions are
// made available to the guest for installation. Valid options are `upload`,
// `attach`, or `disable`. If the mode is `attach` the guest additions ISO will
// be attached as a CD device to the virtual machine. If the mode is `upload`
// made available to the guest for installation. Valid options are upload,
// attach, or disable. If the mode is attach the guest additions ISO will
// be attached as a CD device to the virtual machine. If the mode is upload
// the guest additions ISO will be uploaded to the path specified by
// `guest_additions_path`. The default value is `upload`. If `disable` is used,
// guest_additions_path. The default value is upload. If disable is used,
// guest additions won't be downloaded, either.
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
// The interface type to use to mount guest additions when
// guest_additions_mode is set to attach. Will default to the value set in
// iso_interface, if iso_interface is set. Will default to "ide", if
// iso_interface is not set. Options are "ide" and "sata".
GuestAdditionsInterface string `mapstructure:"guest_additions_interface" required:"false"`
// The path on the guest virtual machine
// where the VirtualBox guest additions ISO will be uploaded. By default this
// is `VBoxGuestAdditions.iso` which should upload into the login directory of
// the user. This is a [configuration
// template](/docs/templates/engine) where the `Version`
// variable is replaced with the VirtualBox version.
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
// The SHA256 checksum of the guest
// additions ISO that will be uploaded to the guest VM. By default the
// checksums will be downloaded from the VirtualBox website, so this only needs
// to be set if you want to be explicit about the checksum.
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
// The URL of the guest additions ISO
// to upload. This can also be a file URL if the ISO is at a local path. By
// default, the VirtualBox builder will attempt to find the guest additions ISO
// on the local file system. If it is not available locally, the builder will
// download the proper guest additions ISO from the internet.
GuestAdditionsURL string `mapstructure:"guest_additions_url" required:"false"`
GuestAdditionsMode string `mapstructure:"guest_additions_mode" required:"false"`
}
func (c *GuestAdditionsConfig) Prepare(ctx *interpolate.Context) []error {
@@ -60,36 +36,5 @@ func (c *GuestAdditionsConfig) Prepare(ctx *interpolate.Context) []error {
"'disable' when communicator = 'none'."))
}
if c.GuestAdditionsMode == "" {
c.GuestAdditionsMode = "upload"
}
if c.GuestAdditionsPath == "" {
c.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
if c.GuestAdditionsSHA256 != "" {
c.GuestAdditionsSHA256 = strings.ToLower(c.GuestAdditionsSHA256)
}
validMode := false
validModes := []string{
GuestAdditionsModeDisable,
GuestAdditionsModeAttach,
GuestAdditionsModeUpload,
}
for _, mode := range validModes {
if c.GuestAdditionsMode == mode {
validMode = true
break
}
}
if !validMode {
errs = append(errs,
fmt.Errorf("guest_additions_mode is invalid. Must be one of: %v", validModes))
}
return errs
}
@@ -0,0 +1,105 @@
package common
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// This step attaches the VirtualBox guest additions as a inserted CD onto
// the virtual machine.
//
// Uses:
// config *config
// driver Driver
// guest_additions_path string
// ui packer.Ui
// vmName string
//
// Produces:
type StepAttachGuestAdditions struct {
attachedPath string
GuestAdditionsMode string
GuestAdditionsInterface string
}
func (s *StepAttachGuestAdditions) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string)
// If we're not attaching the guest additions then just return
if s.GuestAdditionsMode != GuestAdditionsModeAttach {
log.Println("Not attaching guest additions since we're uploading.")
return multistep.ActionContinue
}
// Get the guest additions path since we're doing it
guestAdditionsPath := state.Get("guest_additions_path").(string)
// Attach the guest additions to the computer
controllerName := "IDE Controller"
port := "1"
device := "0"
if s.GuestAdditionsInterface == "sata" {
controllerName = "SATA Controller"
port = "2"
device = "0"
}
log.Println("Attaching guest additions ISO onto IDE controller...")
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--type", "dvddrive",
"--medium", guestAdditionsPath,
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error attaching guest additions: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Track the path so that we can unregister it from VirtualBox later
s.attachedPath = guestAdditionsPath
state.Put("guest_additions_attached", true)
return multistep.ActionContinue
}
func (s *StepAttachGuestAdditions) Cleanup(state multistep.StateBag) {
if s.attachedPath == "" {
return
}
driver := state.Get("driver").(Driver)
vmName := state.Get("vmName").(string)
controllerName := "IDE Controller"
port := "1"
device := "0"
if s.GuestAdditionsInterface == "sata" {
controllerName = "SATA Controller"
port = "2"
device = "0"
}
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--medium", "none",
}
// Remove the ISO. Note that this will probably fail since
// stepRemoveDevices does this as well. No big deal.
driver.VBoxManage(command...)
}
@@ -1,158 +0,0 @@
package common
import (
"context"
"fmt"
"log"
"path/filepath"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// This step attaches the boot ISO, cd_files iso, and guest additions to the
// virtual machine, if present.
type StepAttachISOs struct {
AttachBootISO bool
ISOInterface string
GuestAdditionsMode string
GuestAdditionsInterface string
diskUnmountCommands map[string][]string
}
func (s *StepAttachISOs) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
// Check whether there is anything to attach
ui := state.Get("ui").(packer.Ui)
ui.Say("Mounting ISOs...")
diskMountMap := map[string]string{}
s.diskUnmountCommands = map[string][]string{}
// Track the bootable iso (only used in virtualbox-iso builder. )
if s.AttachBootISO {
isoPath := state.Get("iso_path").(string)
diskMountMap["boot_iso"] = isoPath
}
// Determine if we even have a cd_files disk to attach
if cdPathRaw, ok := state.GetOk("cd_path"); ok {
cdFilesPath := cdPathRaw.(string)
diskMountMap["cd_files"] = cdFilesPath
}
// Determine if we have guest additions to attach
if s.GuestAdditionsMode != GuestAdditionsModeAttach {
log.Println("Not attaching guest additions since we're uploading.")
} else {
// Get the guest additions path since we're doing it
guestAdditionsPath := state.Get("guest_additions_path").(string)
diskMountMap["guest_additions"] = guestAdditionsPath
}
if len(diskMountMap) == 0 {
ui.Message("No ISOs to mount; continuing...")
return multistep.ActionContinue
}
driver := state.Get("driver").(Driver)
vmName := state.Get("vmName").(string)
for diskCategory, isoPath := range diskMountMap {
// If it's a symlink, resolve it to its target.
resolvedIsoPath, err := filepath.EvalSymlinks(isoPath)
if err != nil {
err := fmt.Errorf("Error resolving symlink for ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
isoPath = resolvedIsoPath
// We have three different potential isos we can attach, so let's
// assign each one its own spot so they don't conflict.
var controllerName, device, port string
switch diskCategory {
case "boot_iso":
// figure out controller path
controllerName = "IDE Controller"
port = "0"
device = "1"
if s.ISOInterface == "sata" {
controllerName = "SATA Controller"
port = "1"
device = "0"
}
ui.Message("Mounting boot ISO...")
case "guest_additions":
controllerName = "IDE Controller"
port = "1"
device = "0"
if s.GuestAdditionsInterface == "sata" {
controllerName = "SATA Controller"
port = "2"
device = "0"
}
ui.Message("Mounting guest additions ISO...")
case "cd_files":
controllerName = "IDE Controller"
port = "1"
device = "1"
if s.ISOInterface == "sata" {
controllerName = "SATA Controller"
port = "3"
device = "0"
}
ui.Message("Mounting cd_files ISO...")
}
// Attach the disk to the controller
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--type", "dvddrive",
"--medium", isoPath,
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error attaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Track the disks we've mounted so we can remove them without having
// to re-derive what was mounted where
unmountCommand := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--type", "dvddrive",
"--medium", "none",
}
s.diskUnmountCommands[diskCategory] = unmountCommand
}
state.Put("disk_unmount_commands", s.diskUnmountCommands)
return multistep.ActionContinue
}
func (s *StepAttachISOs) Cleanup(state multistep.StateBag) {
if len(s.diskUnmountCommands) == 0 {
return
}
driver := state.Get("driver").(Driver)
_, ok := state.GetOk("detached_isos")
if !ok {
for _, command := range s.diskUnmountCommands {
err := driver.VBoxManage(command...)
if err != nil {
log.Printf("error detaching iso: %s", err)
}
}
}
}
@@ -21,7 +21,8 @@ import (
//
// Produces:
type StepRemoveDevices struct {
Bundling VBoxBundleConfig
Bundling VBoxBundleConfig
GuestAdditionsInterface string
}
func (s *StepRemoveDevices) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
@@ -72,33 +73,59 @@ func (s *StepRemoveDevices) Run(ctx context.Context, state multistep.StateBag) m
}
}
var isoUnmountCommands map[string][]string
isoUnmountCommandsRaw, ok := state.GetOk("disk_unmount_commands")
if !ok {
// No disks to unmount
return multistep.ActionContinue
} else {
isoUnmountCommands = isoUnmountCommandsRaw.(map[string][]string)
if !s.Bundling.BundleISO {
if _, ok := state.GetOk("attachedIso"); ok {
controllerName := "IDE Controller"
port := "0"
device := "1"
if _, ok := state.GetOk("attachedIsoOnSata"); ok {
controllerName = "SATA Controller"
port = "1"
device = "0"
}
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--medium", "none",
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error detaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
}
for diskCategory, unmountCommand := range isoUnmountCommands {
if diskCategory == "boot_iso" && s.Bundling.BundleISO {
// skip the unmount if user wants to bundle the iso
continue
if _, ok := state.GetOk("guest_additions_attached"); ok {
ui.Message("Removing guest additions drive...")
controllerName := "IDE Controller"
port := "1"
device := "0"
if s.GuestAdditionsInterface == "sata" {
controllerName = "SATA Controller"
port = "2"
device = "0"
}
if err := driver.VBoxManage(unmountCommand...); err != nil {
err := fmt.Errorf("Error detaching ISO: %s", err)
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--medium", "none",
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error removing guest additions: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// log that we removed the isos, so we don't waste time trying to do it
// in the step_attach_isos cleanup.
state.Put("detached_isos", true)
return multistep.ActionContinue
}
@@ -37,17 +37,7 @@ func TestStepRemoveDevices_attachedIso(t *testing.T) {
state := testState(t)
step := new(StepRemoveDevices)
diskUnmountCommands := map[string][]string{
"boot_iso": []string{
"storageattach", "myvm",
"--storagectl", "IDE Controller",
"--port", "0",
"--device", "1",
"--type", "dvddrive",
"--medium", "none",
},
}
state.Put("disk_unmount_commands", diskUnmountCommands)
state.Put("attachedIso", true)
state.Put("vmName", "foo")
driver := state.Get("driver").(*DriverMock)
@@ -73,17 +63,8 @@ func TestStepRemoveDevices_attachedIsoOnSata(t *testing.T) {
state := testState(t)
step := new(StepRemoveDevices)
diskUnmountCommands := map[string][]string{
"boot_iso": []string{
"storageattach", "myvm",
"--storagectl", "SATA Controller",
"--port", "0",
"--device", "1",
"--type", "dvddrive",
"--medium", "none",
},
}
state.Put("disk_unmount_commands", diskUnmountCommands)
state.Put("attachedIso", true)
state.Put("attachedIsoOnSata", true)
state.Put("vmName", "foo")
driver := state.Get("driver").(*DriverMock)
+59 -10
View File
@@ -7,6 +7,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"github.com/hashicorp/hcl/v2/hcldec"
vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common"
@@ -31,7 +32,6 @@ type Config struct {
common.HTTPConfig `mapstructure:",squash"`
common.ISOConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
common.CDConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
vboxcommon.ExportConfig `mapstructure:",squash"`
vboxcommon.OutputConfig `mapstructure:",squash"`
@@ -46,6 +46,29 @@ type Config struct {
// The size, in megabytes, of the hard disk to create for the VM. By
// default, this is 40000 (about 40 GB).
DiskSize uint `mapstructure:"disk_size" required:"false"`
// The path on the guest virtual machine where the VirtualBox guest
// additions ISO will be uploaded. By default this is
// VBoxGuestAdditions.iso which should upload into the login directory of
// the user. This is a configuration template where the `{{ .Version }}`
// variable is replaced with the VirtualBox version.
GuestAdditionsPath string `mapstructure:"guest_additions_path" required:"false"`
// The SHA256 checksum of the guest additions ISO that will be uploaded to
// the guest VM. By default the checksums will be downloaded from the
// VirtualBox website, so this only needs to be set if you want to be
// explicit about the checksum.
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256" required:"false"`
// The URL to the guest additions ISO to upload. This can also be a file
// URL if the ISO is at a local path. By default, the VirtualBox builder
// will attempt to find the guest additions ISO on the local file system.
// If it is not available locally, the builder will download the proper
// guest additions ISO from the internet. This is a template engine, and you
// have access to the variable `{{ .Version }}`.
GuestAdditionsURL string `mapstructure:"guest_additions_url" required:"false"`
// The interface type to use to mount guest additions when
// guest_additions_mode is set to attach. Will default to the value set in
// iso_interface, if iso_interface is set. Will default to "ide", if
// iso_interface is not set. Options are "ide" and "sata".
GuestAdditionsInterface string `mapstructure:"guest_additions_interface" required:"false"`
// The guest OS type being installed. By default this is other, but you can
// get dramatic performance improvements by setting this to the proper
// value. To view all available values for this run VBoxManage list
@@ -144,7 +167,6 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
errs = packer.MultiErrorAppend(errs, b.config.ExportConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.ExportConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.FloppyConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(errs, b.config.CDConfig.Prepare(&b.config.ctx)...)
errs = packer.MultiErrorAppend(
errs, b.config.OutputConfig.Prepare(&b.config.ctx, &b.config.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, b.config.HTTPConfig.Prepare(&b.config.ctx)...)
@@ -162,6 +184,14 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
b.config.DiskSize = 40000
}
if b.config.GuestAdditionsMode == "" {
b.config.GuestAdditionsMode = "upload"
}
if b.config.GuestAdditionsPath == "" {
b.config.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
if b.config.HardDriveInterface == "" {
b.config.HardDriveInterface = "ide"
}
@@ -214,6 +244,29 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
errs, errors.New("iso_interface can only be ide or sata"))
}
validMode := false
validModes := []string{
vboxcommon.GuestAdditionsModeDisable,
vboxcommon.GuestAdditionsModeAttach,
vboxcommon.GuestAdditionsModeUpload,
}
for _, mode := range validModes {
if b.config.GuestAdditionsMode == mode {
validMode = true
break
}
}
if !validMode {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("guest_additions_mode is invalid. Must be one of: %v", validModes))
}
if b.config.GuestAdditionsSHA256 != "" {
b.config.GuestAdditionsSHA256 = strings.ToLower(b.config.GuestAdditionsSHA256)
}
// Warnings
if b.config.ShutdownCommand == "" {
warnings = append(warnings,
@@ -259,10 +312,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Directories: b.config.FloppyConfig.FloppyDirectories,
Label: b.config.FloppyConfig.FloppyLabel,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
new(vboxcommon.StepHTTPIPDiscover),
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
@@ -278,9 +327,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
new(vboxcommon.StepSuppressMessages),
new(stepCreateVM),
new(stepCreateDisk),
&vboxcommon.StepAttachISOs{
AttachBootISO: true,
ISOInterface: b.config.ISOInterface,
new(stepAttachISO),
&vboxcommon.StepAttachGuestAdditions{
GuestAdditionsMode: b.config.GuestAdditionsMode,
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
@@ -338,7 +386,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
ACPIShutdown: b.config.ACPIShutdown,
},
&vboxcommon.StepRemoveDevices{
Bundling: b.config.VBoxBundleConfig,
Bundling: b.config.VBoxBundleConfig,
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManagePost,
+7 -11
View File
@@ -29,8 +29,6 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
@@ -108,12 +106,12 @@ type FlatConfig struct {
VBoxManagePost [][]string `mapstructure:"vboxmanage_post" required:"false" cty:"vboxmanage_post" hcl:"vboxmanage_post"`
VBoxVersionFile *string `mapstructure:"virtualbox_version_file" required:"false" cty:"virtualbox_version_file" hcl:"virtualbox_version_file"`
BundleISO *bool `mapstructure:"bundle_iso" required:"false" cty:"bundle_iso" hcl:"bundle_iso"`
GuestAdditionsMode *string `mapstructure:"guest_additions_mode" cty:"guest_additions_mode" hcl:"guest_additions_mode"`
GuestAdditionsInterface *string `mapstructure:"guest_additions_interface" required:"false" cty:"guest_additions_interface" hcl:"guest_additions_interface"`
GuestAdditionsPath *string `mapstructure:"guest_additions_path" cty:"guest_additions_path" hcl:"guest_additions_path"`
GuestAdditionsSHA256 *string `mapstructure:"guest_additions_sha256" cty:"guest_additions_sha256" hcl:"guest_additions_sha256"`
GuestAdditionsURL *string `mapstructure:"guest_additions_url" required:"false" cty:"guest_additions_url" hcl:"guest_additions_url"`
GuestAdditionsMode *string `mapstructure:"guest_additions_mode" required:"false" cty:"guest_additions_mode" hcl:"guest_additions_mode"`
DiskSize *uint `mapstructure:"disk_size" required:"false" cty:"disk_size" hcl:"disk_size"`
GuestAdditionsPath *string `mapstructure:"guest_additions_path" required:"false" cty:"guest_additions_path" hcl:"guest_additions_path"`
GuestAdditionsSHA256 *string `mapstructure:"guest_additions_sha256" required:"false" cty:"guest_additions_sha256" hcl:"guest_additions_sha256"`
GuestAdditionsURL *string `mapstructure:"guest_additions_url" required:"false" cty:"guest_additions_url" hcl:"guest_additions_url"`
GuestAdditionsInterface *string `mapstructure:"guest_additions_interface" required:"false" cty:"guest_additions_interface" hcl:"guest_additions_interface"`
GuestOSType *string `mapstructure:"guest_os_type" required:"false" cty:"guest_os_type" hcl:"guest_os_type"`
HardDriveDiscard *bool `mapstructure:"hard_drive_discard" required:"false" cty:"hard_drive_discard" hcl:"hard_drive_discard"`
HardDriveInterface *string `mapstructure:"hard_drive_interface" required:"false" cty:"hard_drive_interface" hcl:"hard_drive_interface"`
@@ -158,8 +156,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
@@ -238,11 +234,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"virtualbox_version_file": &hcldec.AttrSpec{Name: "virtualbox_version_file", Type: cty.String, Required: false},
"bundle_iso": &hcldec.AttrSpec{Name: "bundle_iso", Type: cty.Bool, Required: false},
"guest_additions_mode": &hcldec.AttrSpec{Name: "guest_additions_mode", Type: cty.String, Required: false},
"guest_additions_interface": &hcldec.AttrSpec{Name: "guest_additions_interface", Type: cty.String, Required: false},
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
"guest_additions_path": &hcldec.AttrSpec{Name: "guest_additions_path", Type: cty.String, Required: false},
"guest_additions_sha256": &hcldec.AttrSpec{Name: "guest_additions_sha256", Type: cty.String, Required: false},
"guest_additions_url": &hcldec.AttrSpec{Name: "guest_additions_url", Type: cty.String, Required: false},
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
"guest_additions_interface": &hcldec.AttrSpec{Name: "guest_additions_interface", Type: cty.String, Required: false},
"guest_os_type": &hcldec.AttrSpec{Name: "guest_os_type", Type: cty.String, Required: false},
"hard_drive_discard": &hcldec.AttrSpec{Name: "hard_drive_discard", Type: cty.Bool, Required: false},
"hard_drive_interface": &hcldec.AttrSpec{Name: "hard_drive_interface", Type: cty.String, Required: false},
+105
View File
@@ -0,0 +1,105 @@
package iso
import (
"context"
"fmt"
"path/filepath"
vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// This step attaches the ISO to the virtual machine.
//
// Uses:
//
// Produces:
type stepAttachISO struct {
diskPath string
}
func (s *stepAttachISO) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
driver := state.Get("driver").(vboxcommon.Driver)
isoPath := state.Get("iso_path").(string)
ui := state.Get("ui").(packer.Ui)
vmName := state.Get("vmName").(string)
controllerName := "IDE Controller"
port := "0"
device := "1"
if config.ISOInterface == "sata" {
controllerName = "SATA Controller"
port = "1"
device = "0"
}
// If it's a symlink, resolve it to it's target.
resolvedIsoPath, err := filepath.EvalSymlinks(isoPath)
if err != nil {
err := fmt.Errorf("Error resolving symlink for ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
isoPath = resolvedIsoPath
// Attach the disk to the controller
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--type", "dvddrive",
"--medium", isoPath,
}
if err := driver.VBoxManage(command...); err != nil {
err := fmt.Errorf("Error attaching ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Track the path so that we can unregister it from VirtualBox later
s.diskPath = isoPath
// Set some state so we know to remove
state.Put("attachedIso", true)
if controllerName == "SATA Controller" {
state.Put("attachedIsoOnSata", true)
}
return multistep.ActionContinue
}
func (s *stepAttachISO) Cleanup(state multistep.StateBag) {
if s.diskPath == "" {
return
}
config := state.Get("config").(*Config)
driver := state.Get("driver").(vboxcommon.Driver)
vmName := state.Get("vmName").(string)
controllerName := "IDE Controller"
port := "0"
device := "1"
if config.ISOInterface == "sata" {
controllerName = "SATA Controller"
port = "1"
device = "0"
}
command := []string{
"storageattach", vmName,
"--storagectl", controllerName,
"--port", port,
"--device", device,
"--medium", "none",
}
// Remove the ISO. Note that this will probably fail since
// stepRemoveDevices does this as well. No big deal.
driver.VBoxManage(command...)
}
+4 -8
View File
@@ -60,10 +60,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Directories: b.config.FloppyConfig.FloppyDirectories,
Label: b.config.FloppyConfig.FloppyLabel,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
new(vboxcommon.StepHTTPIPDiscover),
&common.StepHTTPServer{
HTTPDir: b.config.HTTPDir,
@@ -95,9 +91,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
ImportFlags: b.config.ImportFlags,
KeepRegistered: b.config.KeepRegistered,
},
&vboxcommon.StepAttachISOs{
AttachBootISO: false,
ISOInterface: b.config.GuestAdditionsInterface,
&vboxcommon.StepAttachGuestAdditions{
GuestAdditionsMode: b.config.GuestAdditionsMode,
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
@@ -154,7 +148,9 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
DisableShutdown: b.config.DisableShutdown,
ACPIShutdown: b.config.ACPIShutdown,
},
&vboxcommon.StepRemoveDevices{},
&vboxcommon.StepRemoveDevices{
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManagePost,
Ctx: b.config.ctx,
+65 -4
View File
@@ -5,6 +5,7 @@ package ovf
import (
"fmt"
"strings"
vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common"
"github.com/hashicorp/packer/common"
@@ -19,7 +20,6 @@ type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
common.CDConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
vboxcommon.ExportConfig `mapstructure:",squash"`
vboxcommon.OutputConfig `mapstructure:",squash"`
@@ -50,6 +50,38 @@ type Config struct {
// this is not recommended since these files can be very large and
// corruption does happen from time to time.
Checksum string `mapstructure:"checksum" required:"true"`
// The method by which guest additions are
// made available to the guest for installation. Valid options are upload,
// attach, or disable. If the mode is attach the guest additions ISO will
// be attached as a CD device to the virtual machine. If the mode is upload
// the guest additions ISO will be uploaded to the path specified by
// guest_additions_path. The default value is upload. If disable is used,
// guest additions won't be downloaded, either.
GuestAdditionsMode string `mapstructure:"guest_additions_mode" required:"false"`
// The path on the guest virtual machine
// where the VirtualBox guest additions ISO will be uploaded. By default this
// is VBoxGuestAdditions.iso which should upload into the login directory of
// the user. This is a configuration
// template where the Version
// variable is replaced with the VirtualBox version.
GuestAdditionsPath string `mapstructure:"guest_additions_path" required:"false"`
// The interface type to use to mount
// guest additions when guest_additions_mode is set to attach. Will
// default to the value set in iso_interface, if iso_interface is set.
// Will default to "ide", if iso_interface is not set. Options are "ide" and
// "sata".
GuestAdditionsInterface string `mapstructure:"guest_additions_interface" required:"false"`
// The SHA256 checksum of the guest
// additions ISO that will be uploaded to the guest VM. By default the
// checksums will be downloaded from the VirtualBox website, so this only needs
// to be set if you want to be explicit about the checksum.
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256" required:"false"`
// The URL to the guest additions ISO
// to upload. This can also be a file URL if the ISO is at a local path. By
// default, the VirtualBox builder will attempt to find the guest additions ISO
// on the local file system. If it is not available locally, the builder will
// download the proper guest additions ISO from the internet.
GuestAdditionsURL string `mapstructure:"guest_additions_url" required:"false"`
// Additional flags to pass to
// VBoxManage import. This can be used to add additional command-line flags
// such as --eula-accept to accept a EULA in the OVF.
@@ -99,6 +131,17 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
// Defaults
if c.GuestAdditionsMode == "" {
c.GuestAdditionsMode = "upload"
}
if c.GuestAdditionsPath == "" {
c.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
if c.GuestAdditionsInterface == "" {
c.GuestAdditionsInterface = "ide"
}
if c.VMName == "" {
c.VMName = fmt.Sprintf(
"packer-%s-%d", c.PackerBuildName, interpolate.InitTime.Unix())
@@ -109,7 +152,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...)
@@ -124,8 +166,27 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required"))
}
if c.GuestAdditionsInterface == "" {
c.GuestAdditionsInterface = "ide"
validMode := false
validModes := []string{
vboxcommon.GuestAdditionsModeDisable,
vboxcommon.GuestAdditionsModeAttach,
vboxcommon.GuestAdditionsModeUpload,
}
for _, mode := range validModes {
if c.GuestAdditionsMode == mode {
validMode = true
break
}
}
if !validMode {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("guest_additions_mode is invalid. Must be one of: %v", validModes))
}
if c.GuestAdditionsSHA256 != "" {
c.GuestAdditionsSHA256 = strings.ToLower(c.GuestAdditionsSHA256)
}
// Warnings
+7 -11
View File
@@ -24,8 +24,6 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
@@ -98,12 +96,12 @@ type FlatConfig struct {
VBoxManage [][]string `mapstructure:"vboxmanage" required:"false" cty:"vboxmanage" hcl:"vboxmanage"`
VBoxManagePost [][]string `mapstructure:"vboxmanage_post" required:"false" cty:"vboxmanage_post" hcl:"vboxmanage_post"`
VBoxVersionFile *string `mapstructure:"virtualbox_version_file" required:"false" cty:"virtualbox_version_file" hcl:"virtualbox_version_file"`
GuestAdditionsMode *string `mapstructure:"guest_additions_mode" cty:"guest_additions_mode" hcl:"guest_additions_mode"`
GuestAdditionsInterface *string `mapstructure:"guest_additions_interface" required:"false" cty:"guest_additions_interface" hcl:"guest_additions_interface"`
GuestAdditionsPath *string `mapstructure:"guest_additions_path" cty:"guest_additions_path" hcl:"guest_additions_path"`
GuestAdditionsSHA256 *string `mapstructure:"guest_additions_sha256" cty:"guest_additions_sha256" hcl:"guest_additions_sha256"`
GuestAdditionsURL *string `mapstructure:"guest_additions_url" required:"false" cty:"guest_additions_url" hcl:"guest_additions_url"`
GuestAdditionsMode *string `mapstructure:"guest_additions_mode" required:"false" cty:"guest_additions_mode" hcl:"guest_additions_mode"`
Checksum *string `mapstructure:"checksum" required:"true" cty:"checksum" hcl:"checksum"`
GuestAdditionsPath *string `mapstructure:"guest_additions_path" required:"false" cty:"guest_additions_path" hcl:"guest_additions_path"`
GuestAdditionsInterface *string `mapstructure:"guest_additions_interface" required:"false" cty:"guest_additions_interface" hcl:"guest_additions_interface"`
GuestAdditionsSHA256 *string `mapstructure:"guest_additions_sha256" required:"false" cty:"guest_additions_sha256" hcl:"guest_additions_sha256"`
GuestAdditionsURL *string `mapstructure:"guest_additions_url" required:"false" cty:"guest_additions_url" hcl:"guest_additions_url"`
ImportFlags []string `mapstructure:"import_flags" required:"false" cty:"import_flags" hcl:"import_flags"`
ImportOpts *string `mapstructure:"import_opts" required:"false" cty:"import_opts" hcl:"import_opts"`
SourcePath *string `mapstructure:"source_path" required:"true" cty:"source_path" hcl:"source_path"`
@@ -140,8 +138,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
@@ -215,11 +211,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"vboxmanage_post": &hcldec.AttrSpec{Name: "vboxmanage_post", Type: cty.List(cty.List(cty.String)), Required: false},
"virtualbox_version_file": &hcldec.AttrSpec{Name: "virtualbox_version_file", Type: cty.String, Required: false},
"guest_additions_mode": &hcldec.AttrSpec{Name: "guest_additions_mode", Type: cty.String, Required: false},
"guest_additions_interface": &hcldec.AttrSpec{Name: "guest_additions_interface", Type: cty.String, Required: false},
"checksum": &hcldec.AttrSpec{Name: "checksum", Type: cty.String, Required: false},
"guest_additions_path": &hcldec.AttrSpec{Name: "guest_additions_path", Type: cty.String, Required: false},
"guest_additions_interface": &hcldec.AttrSpec{Name: "guest_additions_interface", Type: cty.String, Required: false},
"guest_additions_sha256": &hcldec.AttrSpec{Name: "guest_additions_sha256", Type: cty.String, Required: false},
"guest_additions_url": &hcldec.AttrSpec{Name: "guest_additions_url", Type: cty.String, Required: false},
"checksum": &hcldec.AttrSpec{Name: "checksum", Type: cty.String, Required: false},
"import_flags": &hcldec.AttrSpec{Name: "import_flags", Type: cty.List(cty.String), Required: false},
"import_opts": &hcldec.AttrSpec{Name: "import_opts", Type: cty.String, Required: false},
"source_path": &hcldec.AttrSpec{Name: "source_path", Type: cty.String, Required: false},
+2 -10
View File
@@ -54,10 +54,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
Files: b.config.FloppyConfig.FloppyFiles,
Directories: b.config.FloppyConfig.FloppyDirectories,
},
&common.StepCreateCD{
Files: b.config.CDConfig.CDFiles,
Label: b.config.CDConfig.CDLabel,
},
&StepSetSnapshot{
Name: b.config.VMName,
AttachSnapshot: b.config.AttachSnapshot,
@@ -79,11 +75,8 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
&StepImport{
Name: b.config.VMName,
},
&vboxcommon.StepAttachISOs{
AttachBootISO: false,
ISOInterface: b.config.GuestAdditionsInterface,
GuestAdditionsMode: b.config.GuestAdditionsMode,
GuestAdditionsInterface: b.config.GuestAdditionsInterface,
&vboxcommon.StepAttachGuestAdditions{
GuestAdditionsMode: b.config.GuestAdditionsMode,
},
&vboxcommon.StepConfigureVRDP{
VRDPBindAddress: b.config.VRDPBindAddress,
@@ -138,7 +131,6 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
DisableShutdown: b.config.DisableShutdown,
ACPIShutdown: b.config.ACPIShutdown,
},
&vboxcommon.StepRemoveDevices{},
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManagePost,
Ctx: b.config.ctx,
+70 -19
View File
@@ -6,6 +6,7 @@ package vm
import (
"fmt"
"log"
"strings"
"time"
vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common"
@@ -18,19 +19,44 @@ import (
// Config is the configuration structure for the builder.
type Config struct {
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
common.CDConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
vboxcommon.ExportConfig `mapstructure:",squash"`
vboxcommon.OutputConfig `mapstructure:",squash"`
vboxcommon.RunConfig `mapstructure:",squash"`
vboxcommon.CommConfig `mapstructure:",squash"`
vboxcommon.ShutdownConfig `mapstructure:",squash"`
vboxcommon.VBoxManageConfig `mapstructure:",squash"`
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
vboxcommon.GuestAdditionsConfig `mapstructure:",squash"`
common.PackerConfig `mapstructure:",squash"`
common.HTTPConfig `mapstructure:",squash"`
common.FloppyConfig `mapstructure:",squash"`
bootcommand.BootConfig `mapstructure:",squash"`
vboxcommon.ExportConfig `mapstructure:",squash"`
vboxcommon.OutputConfig `mapstructure:",squash"`
vboxcommon.RunConfig `mapstructure:",squash"`
vboxcommon.CommConfig `mapstructure:",squash"`
vboxcommon.ShutdownConfig `mapstructure:",squash"`
vboxcommon.VBoxManageConfig `mapstructure:",squash"`
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
// The method by which guest additions are
// made available to the guest for installation. Valid options are `upload`,
// `attach`, or `disable`. If the mode is `attach` the guest additions ISO will
// be attached as a CD device to the virtual machine. If the mode is `upload`
// the guest additions ISO will be uploaded to the path specified by
// `guest_additions_path`. The default value is `upload`. If `disable` is used,
// guest additions won't be downloaded, either.
GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
// The path on the guest virtual machine
// where the VirtualBox guest additions ISO will be uploaded. By default this
// is `VBoxGuestAdditions.iso` which should upload into the login directory of
// the user. This is a [configuration
// template](/docs/templates/engine) where the `Version`
// variable is replaced with the VirtualBox version.
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
// The SHA256 checksum of the guest
// additions ISO that will be uploaded to the guest VM. By default the
// checksums will be downloaded from the VirtualBox website, so this only needs
// to be set if you want to be explicit about the checksum.
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
// The URL to the guest additions ISO
// to upload. This can also be a file URL if the ISO is at a local path. By
// default, the VirtualBox builder will attempt to find the guest additions ISO
// on the local file system. If it is not available locally, the builder will
// download the proper guest additions ISO from the internet.
GuestAdditionsURL string `mapstructure:"guest_additions_url" required:"false"`
// This is the name of the virtual machine to which the
// builder shall attach.
VMName string `mapstructure:"vm_name" required:"true"`
@@ -83,6 +109,14 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
}
// Defaults
if c.GuestAdditionsMode == "" {
c.GuestAdditionsMode = "upload"
}
if c.GuestAdditionsPath == "" {
c.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
if c.PostShutdownDelay == 0 {
c.PostShutdownDelay = 2 * time.Second
}
@@ -91,7 +125,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
var errs *packer.MultiError
errs = packer.MultiErrorAppend(errs, c.ExportConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.FloppyConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.CDConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.HTTPConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.OutputConfig.Prepare(&c.ctx, &c.PackerConfig)...)
errs = packer.MultiErrorAppend(errs, c.RunConfig.Prepare(&c.ctx)...)
@@ -100,11 +133,6 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
errs = packer.MultiErrorAppend(errs, c.VBoxManageConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.VBoxVersionConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.BootConfig.Prepare(&c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.GuestAdditionsConfig.Prepare(&c.ctx)...)
if c.GuestAdditionsInterface == "" {
c.GuestAdditionsInterface = "ide"
}
log.Printf("PostShutdownDelay: %s", c.PostShutdownDelay)
@@ -113,6 +141,29 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
fmt.Errorf("vm_name is required"))
}
validMode := false
validModes := []string{
vboxcommon.GuestAdditionsModeDisable,
vboxcommon.GuestAdditionsModeAttach,
vboxcommon.GuestAdditionsModeUpload,
}
for _, mode := range validModes {
if c.GuestAdditionsMode == mode {
validMode = true
break
}
}
if !validMode {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("guest_additions_mode is invalid. Must be one of: %v", validModes))
}
if c.GuestAdditionsSHA256 != "" {
c.GuestAdditionsSHA256 = strings.ToLower(c.GuestAdditionsSHA256)
}
// Warnings
var warnings []string
if c.TargetSnapshot == "" && c.SkipExport {
-6
View File
@@ -24,8 +24,6 @@ type FlatConfig struct {
FloppyFiles []string `mapstructure:"floppy_files" cty:"floppy_files" hcl:"floppy_files"`
FloppyDirectories []string `mapstructure:"floppy_dirs" cty:"floppy_dirs" hcl:"floppy_dirs"`
FloppyLabel *string `mapstructure:"floppy_label" cty:"floppy_label" hcl:"floppy_label"`
CDFiles []string `mapstructure:"cd_files" cty:"cd_files" hcl:"cd_files"`
CDLabel *string `mapstructure:"cd_label" cty:"cd_label" hcl:"cd_label"`
BootGroupInterval *string `mapstructure:"boot_keygroup_interval" cty:"boot_keygroup_interval" hcl:"boot_keygroup_interval"`
BootWait *string `mapstructure:"boot_wait" cty:"boot_wait" hcl:"boot_wait"`
BootCommand []string `mapstructure:"boot_command" cty:"boot_command" hcl:"boot_command"`
@@ -99,7 +97,6 @@ type FlatConfig struct {
VBoxManagePost [][]string `mapstructure:"vboxmanage_post" required:"false" cty:"vboxmanage_post" hcl:"vboxmanage_post"`
VBoxVersionFile *string `mapstructure:"virtualbox_version_file" required:"false" cty:"virtualbox_version_file" hcl:"virtualbox_version_file"`
GuestAdditionsMode *string `mapstructure:"guest_additions_mode" cty:"guest_additions_mode" hcl:"guest_additions_mode"`
GuestAdditionsInterface *string `mapstructure:"guest_additions_interface" required:"false" cty:"guest_additions_interface" hcl:"guest_additions_interface"`
GuestAdditionsPath *string `mapstructure:"guest_additions_path" cty:"guest_additions_path" hcl:"guest_additions_path"`
GuestAdditionsSHA256 *string `mapstructure:"guest_additions_sha256" cty:"guest_additions_sha256" hcl:"guest_additions_sha256"`
GuestAdditionsURL *string `mapstructure:"guest_additions_url" required:"false" cty:"guest_additions_url" hcl:"guest_additions_url"`
@@ -138,8 +135,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"floppy_files": &hcldec.AttrSpec{Name: "floppy_files", Type: cty.List(cty.String), Required: false},
"floppy_dirs": &hcldec.AttrSpec{Name: "floppy_dirs", Type: cty.List(cty.String), Required: false},
"floppy_label": &hcldec.AttrSpec{Name: "floppy_label", Type: cty.String, Required: false},
"cd_files": &hcldec.AttrSpec{Name: "cd_files", Type: cty.List(cty.String), Required: false},
"cd_label": &hcldec.AttrSpec{Name: "cd_label", Type: cty.String, Required: false},
"boot_keygroup_interval": &hcldec.AttrSpec{Name: "boot_keygroup_interval", Type: cty.String, Required: false},
"boot_wait": &hcldec.AttrSpec{Name: "boot_wait", Type: cty.String, Required: false},
"boot_command": &hcldec.AttrSpec{Name: "boot_command", Type: cty.List(cty.String), Required: false},
@@ -213,7 +208,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
"vboxmanage_post": &hcldec.AttrSpec{Name: "vboxmanage_post", Type: cty.List(cty.List(cty.String)), Required: false},
"virtualbox_version_file": &hcldec.AttrSpec{Name: "virtualbox_version_file", Type: cty.String, Required: false},
"guest_additions_mode": &hcldec.AttrSpec{Name: "guest_additions_mode", Type: cty.String, Required: false},
"guest_additions_interface": &hcldec.AttrSpec{Name: "guest_additions_interface", Type: cty.String, Required: false},
"guest_additions_path": &hcldec.AttrSpec{Name: "guest_additions_path", Type: cty.String, Required: false},
"guest_additions_sha256": &hcldec.AttrSpec{Name: "guest_additions_sha256", Type: cty.String, Required: false},
"guest_additions_url": &hcldec.AttrSpec{Name: "guest_additions_url", Type: cty.String, Required: false},
+42
View File
@@ -0,0 +1,42 @@
//go:generate struct-markdown
package common
import (
"fmt"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/template/interpolate"
)
type BootConfigWrapper struct {
bootcommand.VNCConfig `mapstructure:",squash"`
// If set to true, Packer will use USB HID Keyboard scan codes to send the boot command to the VM and
// the [disable_vnc](#disable_vnc) option will be ignored and automatically set to true.
// This option is not supported by hosts with free license.
//
// ~> **Note:** The ESXi 6.5+ removes support to VNC. In this case, the `usb_keyboard` should be set to true
// in order to send boot command keystrokes to the VM.
USBKeyBoard bool `mapstructure:"usb_keyboard"`
}
func (c *BootConfigWrapper) Prepare(ctx *interpolate.Context, driverConfig *DriverConfig) (warnings []string, errs []error) {
if c.USBKeyBoard {
if driverConfig.RemoteType == "" {
warnings = append(warnings, "[WARN] `usb_keyboard` can only be used with remote VMWare builds. "+
"The `usb_keyboard` option will be ignored and automatically set to false.")
c.USBKeyBoard = false
} else if !c.DisableVNC {
warnings = append(warnings, "[WARN] `usb_keyboard` is set to true then the remote VMWare builds "+
"will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true.")
c.DisableVNC = true
return
}
}
if len(c.BootCommand) > 0 && c.DisableVNC {
errs = append(errs,
fmt.Errorf("A boot command cannot be used when vnc is disabled."))
}
errs = append(errs, c.BootConfig.Prepare(ctx)...)
return
}
+106
View File
@@ -0,0 +1,106 @@
package common
import (
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/template/interpolate"
)
func TestVNCConfigWrapper_Prepare(t *testing.T) {
tc := []struct {
name string
config *BootConfigWrapper
expectedConfig *BootConfigWrapper
driver *DriverConfig
errs []error
warnings []string
}{
{
name: "VNC and boot command for local build",
config: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
BootConfig: bootcommand.BootConfig{
BootCommand: []string{"<boot><command>"},
},
DisableVNC: true,
},
USBKeyBoard: false,
},
expectedConfig: nil,
driver: new(DriverConfig),
errs: []error{fmt.Errorf("A boot command cannot be used when vnc is disabled.")},
warnings: nil,
},
{
name: "Disable VNC warning for remote build",
config: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
BootConfig: bootcommand.BootConfig{
BootCommand: []string{"<boot><command>"},
},
DisableVNC: false,
},
USBKeyBoard: true,
},
expectedConfig: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
DisableVNC: true,
},
USBKeyBoard: true,
},
driver: &DriverConfig{
RemoteType: "esxi",
},
errs: nil,
warnings: []string{"[WARN] `usb_keyboard` is set to true then the remote VMWare builds " +
"will not use VNC to connect to the host. The `disable_vnc` option will be ignored and automatically set to true."},
},
{
name: "Disable USBKeyBoard warning for local build",
config: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
BootConfig: bootcommand.BootConfig{
BootCommand: []string{"<boot><command>"},
},
DisableVNC: false,
},
USBKeyBoard: true,
},
expectedConfig: &BootConfigWrapper{
VNCConfig: bootcommand.VNCConfig{
DisableVNC: false,
},
USBKeyBoard: false,
},
driver: &DriverConfig{},
errs: nil,
warnings: []string{"[WARN] `usb_keyboard` can only be used with remote VMWare builds. " +
"The `usb_keyboard` option will be ignored and automatically set to false."},
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
warnings, errs := c.config.Prepare(interpolate.NewContext(), c.driver)
if !reflect.DeepEqual(errs, c.errs) {
t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs)
}
if diff := cmp.Diff(warnings, c.warnings); diff != "" {
t.Fatalf("unexpected warnings: %s", diff)
}
if len(c.errs) == 0 {
if diff := cmp.Diff(c.config, c.expectedConfig,
cmpopts.IgnoreFields(bootcommand.VNCConfig{},
"BootConfig",
)); diff != "" {
t.Fatalf("unexpected config: %s", diff)
}
}
})
}
}
+26 -6
View File
@@ -101,17 +101,37 @@ func NewDriver(dconfig *DriverConfig, config *SSHConfig, vmName string) (Driver,
switch runtime.GOOS {
case "darwin":
drivers = []Driver{
NewFusion6Driver(dconfig, config),
NewFusion5Driver(dconfig, config),
&Fusion6Driver{
Fusion5Driver: Fusion5Driver{
AppPath: dconfig.FusionAppPath,
SSHConfig: config,
},
},
&Fusion5Driver{
AppPath: dconfig.FusionAppPath,
SSHConfig: config,
},
}
case "linux":
fallthrough
case "windows":
drivers = []Driver{
NewWorkstation10Driver(config),
NewWorkstation9Driver(config),
NewPlayer6Driver(config),
NewPlayer5Driver(config),
&Workstation10Driver{
Workstation9Driver: Workstation9Driver{
SSHConfig: config,
},
},
&Workstation9Driver{
SSHConfig: config,
},
&Player6Driver{
Player5Driver: Player5Driver{
SSHConfig: config,
},
},
&Player5Driver{
SSHConfig: config,
},
}
default:
return nil, fmt.Errorf("can't find driver for OS: %s", runtime.GOOS)
+38 -10
View File
@@ -20,20 +20,21 @@ import (
"strings"
"time"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
"github.com/hashicorp/go-getter/v2"
"github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
helperssh "github.com/hashicorp/packer/helper/ssh"
"github.com/hashicorp/packer/packer"
"github.com/vmware/govmomi"
"github.com/vmware/govmomi/find"
"github.com/vmware/govmomi/session"
"github.com/vmware/govmomi/vim25"
"github.com/vmware/govmomi/vim25/methods"
"github.com/vmware/govmomi/vim25/soap"
"github.com/vmware/govmomi/vim25/types"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/mobile/event/key"
)
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
@@ -950,10 +951,37 @@ func (r *esxcliReader) find(key, val string) (map[string]string, error) {
}
}
func (d *ESX5Driver) AcquireVNCOverWebsocketTicket() (*types.VirtualMachineTicket, error) {
type KeyInput struct {
Scancode key.Code
Alt bool
Ctrl bool
Shift bool
}
func (d *ESX5Driver) TypeOnKeyboard(input KeyInput) (int32, error) {
vm, err := d.finder.VirtualMachine(d.ctx, d.VMName)
if err != nil {
return nil, err
return 0, err
}
return vm.AcquireTicket(d.ctx, string(types.VirtualMachineTicketTypeWebmks))
var spec types.UsbScanCodeSpec
spec.KeyEvents = append(spec.KeyEvents, types.UsbScanCodeSpecKeyEvent{
UsbHidCode: int32(input.Scancode)<<16 | 7,
Modifiers: &types.UsbScanCodeSpecModifierType{
LeftControl: &input.Ctrl,
LeftAlt: &input.Alt,
LeftShift: &input.Shift,
},
})
req := &types.PutUsbScanCodes{
This: vm.Reference(),
Spec: spec,
}
resp, err := methods.PutUsbScanCodes(d.ctx, d.client.RoundTripper, req)
if err != nil {
return 0, err
}
return resp.Returnval, nil
}
-7
View File
@@ -24,13 +24,6 @@ type Fusion5Driver struct {
SSHConfig *SSHConfig
}
func NewFusion5Driver(dconfig *DriverConfig, config *SSHConfig) Driver {
return &Fusion5Driver{
AppPath: dconfig.FusionAppPath,
SSHConfig: config,
}
}
func (d *Fusion5Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with Fusion 5. Please use Fusion 6+.")
}
-9
View File
@@ -18,15 +18,6 @@ type Fusion6Driver struct {
Fusion5Driver
}
func NewFusion6Driver(dconfig *DriverConfig, config *SSHConfig) Driver {
return &Fusion6Driver{
Fusion5Driver: Fusion5Driver{
AppPath: dconfig.FusionAppPath,
SSHConfig: config,
},
}
}
func (d *Fusion6Driver) Clone(dst, src string, linked bool) error {
var cloneType string
-6
View File
@@ -25,12 +25,6 @@ type Player5Driver struct {
SSHConfig *SSHConfig
}
func NewPlayer5Driver(config *SSHConfig) Driver {
return &Player5Driver{
SSHConfig: config,
}
}
func (d *Player5Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with VMWare Player version 5. Please use VMWare Player version 6, or greater.")
}
-8
View File
@@ -13,14 +13,6 @@ type Player6Driver struct {
Player5Driver
}
func NewPlayer6Driver(config *SSHConfig) Driver {
return &Player6Driver{
Player5Driver: Player5Driver{
SSHConfig: config,
},
}
}
func (d *Player6Driver) Clone(dst, src string, linked bool) error {
// TODO(rasa) check if running player+, not just player
@@ -13,14 +13,6 @@ type Workstation10Driver struct {
Workstation9Driver
}
func NewWorkstation10Driver(config *SSHConfig) Driver {
return &Workstation10Driver{
Workstation9Driver: Workstation9Driver{
SSHConfig: config,
},
}
}
func (d *Workstation10Driver) Clone(dst, src string, linked bool) error {
var cloneType string
@@ -24,12 +24,6 @@ type Workstation9Driver struct {
SSHConfig *SSHConfig
}
func NewWorkstation9Driver(config *SSHConfig) Driver {
return &Workstation9Driver{
SSHConfig: config,
}
}
func (d *Workstation9Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with VMware WS version 9. Please use VMware WS version 10, or greater.")
}
+17 -35
View File
@@ -8,7 +8,7 @@ import (
"github.com/hashicorp/packer/template/interpolate"
)
// ~> **Note:** If [vnc_over_websocket](#vnc_over_websocket) is set to true, any other VNC configuration will be ignored.
// ~> **Note:** If [usb_keyboard](#usb_keyboard) is set to true, any VNC configuration will be ignored.
type RunConfig struct {
// Packer defaults to building VMware virtual machines
// by launching a GUI that shows the console of the machine being built. When
@@ -38,46 +38,28 @@ type RunConfig struct {
// true if building on ESXi 6.5 and 6.7 with VNC enabled. Defaults to
// false.
VNCDisablePassword bool `mapstructure:"vnc_disable_password" required:"false"`
// When set to true, Packer will connect to the remote VNC server over a websocket connection
// and any other VNC configuration option will be ignored.
// Remote builds using ESXi 6.7+ allows to connect to the VNC server only over websocket,
// for these the `vnc_over_websocket` must be set to true.
VNCOverWebsocket bool `mapstructure:"vnc_over_websocket" required:"false"`
// Do not validate VNC over websocket server's TLS certificate. Defaults to `false`.
InsecureConnection bool `mapstructure:"insecure_connection" required:"false"`
}
func (c *RunConfig) Prepare(_ *interpolate.Context, driverConfig *DriverConfig) (warnings []string, errs []error) {
if c.VNCOverWebsocket {
if driverConfig.RemoteType == "" {
errs = append(errs, fmt.Errorf("'vnc_over_websocket' can only be used with remote VMWare builds."))
return
func (c *RunConfig) Prepare(_ *interpolate.Context, bootConfig *BootConfigWrapper) (errs []error) {
if !bootConfig.USBKeyBoard {
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
if c.VNCPortMin != 0 || c.VNCPortMax != 0 || c.VNCBindAddress != "" || c.VNCDisablePassword {
warnings = append(warnings, "[WARN] When 'vnc_over_websocket' is set "+
"any other VNC configuration will be ignored.")
if c.VNCPortMax == 0 {
c.VNCPortMax = 6000
}
return
}
if c.VNCPortMin == 0 {
c.VNCPortMin = 5900
}
if c.VNCBindAddress == "" {
c.VNCBindAddress = "127.0.0.1"
}
if c.VNCPortMax == 0 {
c.VNCPortMax = 6000
if c.VNCPortMin > c.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if c.VNCPortMin < 0 {
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
}
}
if c.VNCBindAddress == "" {
c.VNCBindAddress = "127.0.0.1"
}
if c.VNCPortMin > c.VNCPortMax {
errs = append(errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
}
if c.VNCPortMin < 0 {
errs = append(errs, fmt.Errorf("vnc_port_min must be positive"))
}
return
}
-119
View File
@@ -1,119 +0,0 @@
package common
import (
"fmt"
"reflect"
"testing"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/template/interpolate"
)
func TestRunConfig_Prepare(t *testing.T) {
tc := []struct {
name string
config *RunConfig
expectedConfig *RunConfig
driver *DriverConfig
errs []error
warnings []string
}{
{
name: "VNC dafaults",
config: &RunConfig{},
expectedConfig: &RunConfig{
VNCPortMin: 5900,
VNCPortMax: 6000,
VNCBindAddress: "127.0.0.1",
},
driver: new(DriverConfig),
errs: nil,
warnings: nil,
},
{
name: "VNC port min less than vnc port max",
config: &RunConfig{
VNCPortMin: 5000,
VNCPortMax: 5900,
},
expectedConfig: &RunConfig{
VNCPortMin: 5000,
VNCPortMax: 5900,
VNCBindAddress: "127.0.0.1",
},
driver: new(DriverConfig),
errs: nil,
warnings: nil,
},
{
name: "VNC port min bigger than vnc port max",
config: &RunConfig{
VNCPortMin: 5900,
VNCPortMax: 5000,
},
expectedConfig: nil,
driver: new(DriverConfig),
errs: []error{fmt.Errorf("vnc_port_min must be less than vnc_port_max")},
warnings: nil,
},
{
name: "VNC port min must be positive",
config: &RunConfig{
VNCPortMin: -1,
},
expectedConfig: nil,
driver: new(DriverConfig),
errs: []error{fmt.Errorf("vnc_port_min must be positive")},
warnings: nil,
},
{
name: "fail when vnc_over_websocket set when remote_type is not set",
config: &RunConfig{
VNCOverWebsocket: true,
},
expectedConfig: nil,
driver: new(DriverConfig),
errs: []error{fmt.Errorf("'vnc_over_websocket' can only be used with remote VMWare builds.")},
warnings: nil,
},
{
name: "warn about ignored vnc configuration",
config: &RunConfig{
VNCOverWebsocket: true,
VNCPortMin: 5000,
VNCPortMax: 5900,
},
expectedConfig: &RunConfig{
VNCOverWebsocket: true,
VNCPortMin: 5000,
VNCPortMax: 5900,
},
driver: &DriverConfig{RemoteType: "esxi"},
errs: nil,
warnings: []string{"[WARN] When 'vnc_over_websocket' is set " +
"any other VNC configuration will be ignored."},
},
}
for _, c := range tc {
t.Run(c.name, func(t *testing.T) {
warnings, errs := c.config.Prepare(interpolate.NewContext(), c.driver)
if !reflect.DeepEqual(errs, c.errs) {
t.Fatalf("bad: \n expected '%v' \nactual '%v'", c.errs, errs)
}
if diff := cmp.Diff(warnings, c.warnings); diff != "" {
t.Fatalf("unexpected warnings: %s", diff)
}
if len(c.errs) == 0 {
if diff := cmp.Diff(c.config, c.expectedConfig,
cmpopts.IgnoreFields(bootcommand.VNCConfig{},
"BootConfig",
)); diff != "" {
t.Fatalf("unexpected config: %s", diff)
}
}
})
}
}
+4 -98
View File
@@ -20,12 +20,10 @@ import (
// Produces:
// display_name string - Value of the displayName key set in the VMX file
type StepConfigureVMX struct {
CustomData map[string]string
DisplayName string
SkipFloppy bool
VMName string
DiskAdapterType string
CDROMAdapterType string
CustomData map[string]string
DisplayName string
SkipFloppy bool
VMName string
}
func (s *StepConfigureVMX) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
@@ -85,17 +83,6 @@ func (s *StepConfigureVMX) Run(ctx context.Context, state multistep.StateBag) mu
state.Put("temporaryDevices", tmpBuildDevices)
}
// Add our custom CD, if it exists
if cdPath, ok := state.GetOk("cd_path"); ok {
if cdPath != "" {
diskAndCDConfigData := DefaultDiskAndCDROMTypes(s.DiskAdapterType, s.CDROMAdapterType)
cdromPrefix := diskAndCDConfigData.CDROMType + "1:" + diskAndCDConfigData.CDROMType_PrimarySecondary
vmxData[cdromPrefix+".present"] = "TRUE"
vmxData[cdromPrefix+".fileName"] = cdPath.(string)
vmxData[cdromPrefix+".deviceType"] = "cdrom-image"
}
}
// If the build is taking place on a remote ESX server, the displayName
// will be needed for discovery of the VM's IP address and for export
// of the VM. The displayName key should always be set in the VMX file,
@@ -138,86 +125,5 @@ func (s *StepConfigureVMX) Run(ctx context.Context, state multistep.StateBag) mu
return multistep.ActionContinue
}
type DiskAndCDConfigData struct {
SCSI_Present string
SCSI_diskAdapterType string
SATA_Present string
NVME_Present string
DiskType string
CDROMType string
CDROMType_PrimarySecondary string
CDROM_PATH string
}
// DefaultDiskAndCDROMTypes takes the disk adapter type and cdrom adapter type from the config and converts them
// into template interpolation data for creating or configuring a vmx.
func DefaultDiskAndCDROMTypes(diskAdapterType string, cdromAdapterType string) DiskAndCDConfigData {
diskData := DiskAndCDConfigData{
SCSI_Present: "FALSE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "scsi",
CDROMType: "ide",
CDROMType_PrimarySecondary: "0",
}
/// Use the disk adapter type that the user specified to tweak the .vmx
// Also sync the cdrom adapter type according to what's common for that disk type.
// XXX: If the cdrom type is modified, make sure to update common/step_clean_vmx.go
// so that it will regex the correct cdrom device for removal.
diskAdapterType = strings.ToLower(diskAdapterType)
switch diskAdapterType {
case "ide":
diskData.DiskType = "ide"
diskData.CDROMType = "ide"
diskData.CDROMType_PrimarySecondary = "1"
case "sata":
diskData.SATA_Present = "TRUE"
diskData.DiskType = "sata"
diskData.CDROMType = "sata"
diskData.CDROMType_PrimarySecondary = "1"
case "nvme":
diskData.NVME_Present = "TRUE"
diskData.DiskType = "nvme"
diskData.SATA_Present = "TRUE"
diskData.CDROMType = "sata"
diskData.CDROMType_PrimarySecondary = "0"
case "scsi":
diskAdapterType = "lsilogic"
fallthrough
default:
diskData.SCSI_Present = "TRUE"
diskData.SCSI_diskAdapterType = diskAdapterType // defaults to lsilogic
diskData.DiskType = "scsi"
diskData.CDROMType = "ide"
diskData.CDROMType_PrimarySecondary = "0"
}
/// Handle the cdrom adapter type. If the disk adapter type and the
// cdrom adapter type are the same, then ensure that the cdrom is the
// secondary device on whatever bus the disk adapter is on.
if cdromAdapterType == "" {
cdromAdapterType = diskData.CDROMType
} else if cdromAdapterType == diskAdapterType {
diskData.CDROMType_PrimarySecondary = "1"
} else {
diskData.CDROMType_PrimarySecondary = "0"
}
switch cdromAdapterType {
case "ide":
diskData.CDROMType = "ide"
case "sata":
diskData.SATA_Present = "TRUE"
diskData.CDROMType = "sata"
case "scsi":
diskData.SCSI_Present = "TRUE"
diskData.CDROMType = "scsi"
}
return diskData
}
func (s *StepConfigureVMX) Cleanup(state multistep.StateBag) {
}
@@ -7,7 +7,6 @@ import (
"testing"
"github.com/hashicorp/packer/helper/multistep"
"github.com/stretchr/testify/assert"
)
func testVMXFile(t *testing.T) string {
@@ -272,108 +271,3 @@ func TestStepConfigureVMX_vmxPathBad(t *testing.T) {
}
}
func TestStepConfigureVMX_DefaultDiskAndCDROMTypes(t *testing.T) {
type testCase struct {
inDiskAdapter string
inCDromAdapter string
expectedOut DiskAndCDConfigData
reason string
}
testcases := []testCase{
{
inDiskAdapter: "",
inCDromAdapter: "",
expectedOut: DiskAndCDConfigData{
SCSI_Present: "TRUE",
SCSI_diskAdapterType: "",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "scsi",
CDROMType: "ide",
CDROMType_PrimarySecondary: "0",
},
reason: "Test that default creases scsi disk with ide cd",
},
{
inDiskAdapter: "ide",
inCDromAdapter: "",
expectedOut: DiskAndCDConfigData{
SCSI_Present: "FALSE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "ide",
CDROMType: "ide",
CDROMType_PrimarySecondary: "1",
},
reason: "ide disk adapter should pass through and not get defaulted to scsi",
},
{
inDiskAdapter: "sata",
inCDromAdapter: "",
expectedOut: DiskAndCDConfigData{
SCSI_Present: "FALSE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "TRUE",
NVME_Present: "FALSE",
DiskType: "sata",
CDROMType: "sata",
CDROMType_PrimarySecondary: "1",
},
reason: "when disk is set to sata, cdromtype should also default to sata",
},
{
inDiskAdapter: "nvme",
inCDromAdapter: "",
expectedOut: DiskAndCDConfigData{
SCSI_Present: "FALSE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "TRUE",
NVME_Present: "TRUE",
DiskType: "nvme",
CDROMType: "sata",
CDROMType_PrimarySecondary: "0",
},
reason: "when disk is set to nvme, cdromtype should default to sata",
},
{
inDiskAdapter: "scsi",
inCDromAdapter: "",
expectedOut: DiskAndCDConfigData{
SCSI_Present: "TRUE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "scsi",
CDROMType: "ide",
CDROMType_PrimarySecondary: "0",
},
reason: "when disk is set to scsi, adapter should default back to lsilogic",
},
{
inDiskAdapter: "scsi",
inCDromAdapter: "scsi",
expectedOut: DiskAndCDConfigData{
SCSI_Present: "TRUE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "scsi",
CDROMType: "scsi",
CDROMType_PrimarySecondary: "0",
},
reason: "when cdrom adapter is set, it should override the default",
},
}
for _, tc := range testcases {
diskConfigData := DefaultDiskAndCDROMTypes(tc.inDiskAdapter, tc.inCDromAdapter)
assert.Equal(t, diskConfigData, tc.expectedOut, tc.reason)
}
}
+1 -6
View File
@@ -45,13 +45,8 @@ func (s *StepCreateDisks) Run(ctx context.Context, state multistep.StateBag) mul
}
// Additional disks
if len(s.AdditionalDiskSize) > 0 {
incrementer := 1
for i, diskSize := range s.AdditionalDiskSize {
// scsi slot 7 is reserved, so we skip it.
if i+incrementer == 7 {
incrementer = 2
}
path := filepath.Join(*s.OutputDir, fmt.Sprintf("%s-%d.vmdk", s.DiskName, i+incrementer))
path := filepath.Join(*s.OutputDir, fmt.Sprintf("%s-%d.vmdk", s.DiskName, i+1))
diskFullPaths = append(diskFullPaths, path)
size := fmt.Sprintf("%dM", uint64(diskSize))
diskSizes = append(diskSizes, size)
+2 -7
View File
@@ -11,7 +11,6 @@ import (
type StepPrepareTools struct {
RemoteType string
ToolsUploadFlavor string
ToolsSourcePath string
}
func (c *StepPrepareTools) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
@@ -21,15 +20,11 @@ func (c *StepPrepareTools) Run(ctx context.Context, state multistep.StateBag) mu
return multistep.ActionContinue
}
if c.ToolsUploadFlavor == "" && c.ToolsSourcePath == "" {
if c.ToolsUploadFlavor == "" {
return multistep.ActionContinue
}
path := c.ToolsSourcePath
if path == "" {
path = driver.ToolsIsoPath(c.ToolsUploadFlavor)
}
path := driver.ToolsIsoPath(c.ToolsUploadFlavor)
if _, err := os.Stat(path); err != nil {
state.Put("error", fmt.Errorf(
"Couldn't find VMware tools for '%s'! VMware often downloads these\n"+
@@ -114,65 +114,3 @@ func TestStepPrepareTools_nonExist(t *testing.T) {
t.Fatal("should NOT have tools_upload_source")
}
}
func TestStepPrepareTools_SourcePath(t *testing.T) {
state := testState(t)
step := &StepPrepareTools{
RemoteType: "",
ToolsSourcePath: "/path/to/tool.iso",
}
driver := state.Get("driver").(*DriverMock)
// Mock results
driver.ToolsIsoPathResult = "foo"
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
t.Fatalf("Should have failed when stat failed %#v", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should have error")
}
// Test the driver
if driver.ToolsIsoPathCalled {
t.Fatal("tools iso path should not be called when ToolsSourcePath is set")
}
// Test the resulting state
if _, ok := state.GetOk("tools_upload_source"); ok {
t.Fatal("should NOT have tools_upload_source")
}
}
func TestStepPrepareTools_SourcePath_exists(t *testing.T) {
state := testState(t)
step := &StepPrepareTools{
RemoteType: "",
ToolsSourcePath: "./step_prepare_tools.go",
}
driver := state.Get("driver").(*DriverMock)
// Mock results
driver.ToolsIsoPathResult = "foo"
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("Step should succeed when stat succeeds: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
// Test the driver
if driver.ToolsIsoPathCalled {
t.Fatal("tools iso path should not be called when ToolsSourcePath is set")
}
// Test the resulting state
if _, ok := state.GetOk("tools_upload_source"); !ok {
t.Fatal("should have tools_upload_source")
}
}
@@ -0,0 +1,126 @@
package common
import (
"context"
"fmt"
"time"
"github.com/hashicorp/packer/common/bootcommand"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"golang.org/x/mobile/event/key"
)
// This step "types" the boot command into the VM using USB Scan Codes.
type StepUSBBootCommand struct {
Config bootcommand.BootConfig
KeyInterval time.Duration
VMName string
Ctx interpolate.Context
}
type USBBootCommandTemplateData struct {
HTTPIP string
HTTPPort int
Name string
}
func (s *StepUSBBootCommand) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
debug := state.Get("debug").(bool)
ui := state.Get("ui").(packer.Ui)
driver := state.Get("driver").(*ESX5Driver)
if s.Config.BootCommand == nil {
return multistep.ActionContinue
}
// Wait the for the vm to boot.
if int64(s.Config.BootWait) > 0 {
ui.Say(fmt.Sprintf("Waiting %s for boot...", s.Config.BootWait.String()))
select {
case <-time.After(s.Config.BootWait):
break
case <-ctx.Done():
return multistep.ActionHalt
}
}
var pauseFn multistep.DebugPauseFn
if debug {
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}
port := state.Get("http_port").(int)
if port > 0 {
ip := state.Get("http_ip").(string)
s.Ctx.Data = &USBBootCommandTemplateData{
HTTPIP: ip,
HTTPPort: port,
Name: s.VMName,
}
ui.Say(fmt.Sprintf("HTTP server is working at http://%v:%v/", ip, port))
}
var keyAlt, keyCtrl, keyShift bool
sendCodes := func(code key.Code, down bool) error {
switch code {
case key.CodeLeftAlt:
keyAlt = down
case key.CodeLeftControl:
keyCtrl = down
case key.CodeLeftShift:
keyShift = down
}
shift := down
if keyShift {
shift = keyShift
}
_, err := driver.TypeOnKeyboard(KeyInput{
Scancode: code,
Ctrl: keyCtrl,
Alt: keyAlt,
Shift: shift,
})
if err != nil {
return fmt.Errorf("error typing a boot command (code, down) `%d, %t`: %w", code, down, err)
}
return nil
}
d := bootcommand.NewUSBDriver(sendCodes, s.KeyInterval)
ui.Say("Typing boot command...")
flatBootCommand := s.Config.FlatBootCommand()
command, err := interpolate.Render(flatBootCommand, &s.Ctx)
if err != nil {
err := fmt.Errorf("Error preparing boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
seq, err := bootcommand.GenerateExpressionSequence(command)
if err != nil {
err := fmt.Errorf("Error generating boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := seq.Do(ctx, d); err != nil {
err := fmt.Errorf("Error running boot command: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if pauseFn != nil {
pauseFn(multistep.DebugLocationAfterRun, fmt.Sprintf("boot_command: %s", command), state)
}
return multistep.ActionContinue
}
func (*StepUSBBootCommand) Cleanup(multistep.StateBag) {}
+37 -3
View File
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
"net"
"time"
"github.com/hashicorp/packer/common/bootcommand"
@@ -43,8 +44,9 @@ func (s *StepVNCBootCommand) Run(ctx context.Context, state multistep.StateBag)
debug := state.Get("debug").(bool)
httpPort := state.Get("http_port").(int)
ui := state.Get("ui").(packer.Ui)
conn := state.Get("vnc_conn").(*vnc.ClientConn)
defer conn.Close()
vncIp := state.Get("vnc_ip").(string)
vncPort := state.Get("vnc_port").(int)
vncPassword := state.Get("vnc_password")
// Wait the for the vm to boot.
if int64(s.Config.BootWait) > 0 {
@@ -56,11 +58,43 @@ func (s *StepVNCBootCommand) Run(ctx context.Context, state multistep.StateBag)
return multistep.ActionHalt
}
}
var pauseFn multistep.DebugPauseFn
if debug {
pauseFn = state.Get("pauseFn").(multistep.DebugPauseFn)
}
// Connect to VNC
ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIp, vncPort))
nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIp, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer nc.Close()
var auth []vnc.ClientAuth
if vncPassword != nil && len(vncPassword.(string)) > 0 {
auth = []vnc.ClientAuth{&vnc.PasswordAuth{Password: vncPassword.(string)}}
} else {
auth = []vnc.ClientAuth{new(vnc.ClientAuthNone)}
}
c, err := vnc.Client(nc, &vnc.ClientConfig{Auth: auth, Exclusive: true})
if err != nil {
err := fmt.Errorf("Error handshaking with VNC: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
defer c.Close()
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
hostIP := state.Get("http_ip").(string)
s.Ctx.Data = &VNCBootCommandTemplateData{
HTTPIP: hostIP,
@@ -68,7 +102,7 @@ func (s *StepVNCBootCommand) Run(ctx context.Context, state multistep.StateBag)
Name: s.VMName,
}
d := bootcommand.NewVNCDriver(conn, s.Config.BootKeyInterval)
d := bootcommand.NewVNCDriver(c, s.Config.BootKeyInterval)
ui.Say("Typing the boot command over VNC...")
flatBootCommand := s.Config.FlatBootCommand()
-141
View File
@@ -1,141 +0,0 @@
package common
import (
"context"
"crypto/tls"
"fmt"
"log"
"net"
"net/url"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/mitchellh/go-vnc"
"golang.org/x/net/websocket"
)
type StepVNCConnect struct {
VNCEnabled bool
VNCOverWebsocket bool
InsecureConnection bool
DriverConfig *DriverConfig
}
func (s *StepVNCConnect) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
if !s.VNCOverWebsocket && !s.VNCEnabled {
return multistep.ActionContinue
}
ui := state.Get("ui").(packer.Ui)
var c *vnc.ClientConn
var err error
if s.VNCOverWebsocket {
ui.Say("Connecting to VNC over websocket...")
c, err = s.ConnectVNCOverWebsocketClient(state)
} else {
ui.Say("Connecting to VNC...")
c, err = s.ConnectVNC(state)
}
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("vnc_conn", c)
return multistep.ActionContinue
}
func (s *StepVNCConnect) ConnectVNCOverWebsocketClient(state multistep.StateBag) (*vnc.ClientConn, error) {
driver := state.Get("driver").(*ESX5Driver)
// Acquire websocket ticket
ticket, err := driver.AcquireVNCOverWebsocketTicket()
if err != nil {
err := fmt.Errorf("Error acquiring vnc over websocket ticket: %s", err)
state.Put("error", err)
return nil, err
}
host := ticket.Host
if len(host) == 0 {
host = s.DriverConfig.RemoteHost
}
port := ticket.Port
if port == 0 {
port = 443
}
websocketUrl := fmt.Sprintf("wss://%s:%d/ticket/%s", host, port, ticket.Ticket)
log.Printf("[DEBUG] websocket url: %s", websocketUrl)
u, err := url.Parse(websocketUrl)
if err != nil {
err := fmt.Errorf("Error parsing websocket url: %s\n", err)
state.Put("error", err)
return nil, err
}
origin, err := url.Parse("http://localhost")
if err != nil {
err := fmt.Errorf("Error parsing websocket origin url: %s\n", err)
state.Put("error", err)
return nil, err
}
// Create the websocket connection and set it to a BinaryFrame
websocketConfig := &websocket.Config{
Location: u,
Origin: origin,
TlsConfig: &tls.Config{InsecureSkipVerify: s.InsecureConnection},
Version: websocket.ProtocolVersionHybi13,
Protocol: []string{"binary"},
}
nc, err := websocket.DialConfig(websocketConfig)
if err != nil {
err := fmt.Errorf("Error Dialing: %s\n", err)
state.Put("error", err)
return nil, err
}
nc.PayloadType = websocket.BinaryFrame
// Setup the VNC connection over the websocket
ccconfig := &vnc.ClientConfig{
Auth: []vnc.ClientAuth{new(vnc.ClientAuthNone)},
Exclusive: false,
}
c, err := vnc.Client(nc, ccconfig)
if err != nil {
err := fmt.Errorf("Error setting the VNC over websocket client: %s\n", err)
state.Put("error", err)
return nil, err
}
return c, nil
}
func (s *StepVNCConnect) ConnectVNC(state multistep.StateBag) (*vnc.ClientConn, error) {
vncIp := state.Get("vnc_ip").(string)
vncPort := state.Get("vnc_port").(int)
vncPassword := state.Get("vnc_password")
nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIp, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)
return nil, err
}
auth := []vnc.ClientAuth{new(vnc.ClientAuthNone)}
if vncPassword != nil && len(vncPassword.(string)) > 0 {
auth = []vnc.ClientAuth{&vnc.PasswordAuth{Password: vncPassword.(string)}}
}
c, err := vnc.Client(nc, &vnc.ClientConfig{Auth: auth, Exclusive: true})
if err != nil {
err := fmt.Errorf("Error handshaking with VNC: %s", err)
state.Put("error", err)
return nil, err
}
return c, nil
}
func (s *StepVNCConnect) Cleanup(multistep.StateBag) {}
+1 -13
View File
@@ -3,8 +3,6 @@
package common
import (
"fmt"
"github.com/hashicorp/packer/template/interpolate"
)
@@ -20,22 +18,12 @@ type ToolsConfig struct {
// the upload path is set to `{{.Flavor}}.iso`. This setting is not used
// when `remote_type` is `esx5`.
ToolsUploadPath string `mapstructure:"tools_upload_path" required:"false"`
// The path on your local machine to fetch the vmware tools from. If this
// is not set but the tools_upload_flavor is set, then Packer will try to
// load the VMWare tools from the VMWare installation directory.
ToolsSourcePath string `mapstructure:"tools_source_path" required:"false"`
}
func (c *ToolsConfig) Prepare(ctx *interpolate.Context) []error {
errs := []error{}
if c.ToolsUploadPath == "" {
if c.ToolsSourcePath != "" && c.ToolsUploadFlavor == "" {
errs = append(errs, fmt.Errorf("If you provide a "+
"tools_source_path, you must also provide either a "+
"tools_upload_flavor or a tools_upload_path."))
}
c.ToolsUploadPath = "{{ .Flavor }}.iso"
}
return errs
return nil
}
@@ -1,65 +0,0 @@
package common
import (
"testing"
"github.com/hashicorp/packer/template/interpolate"
)
func TestToolsConfigPrepare_Empty(t *testing.T) {
c := &ToolsConfig{}
errs := c.Prepare(interpolate.NewContext())
if len(errs) > 0 {
t.Fatalf("err: %#v", errs)
}
if c.ToolsUploadPath != "{{ .Flavor }}.iso" {
t.Fatal("should have defaulted tools upload path")
}
}
func TestToolsConfigPrepare_SetUploadPath(t *testing.T) {
c := &ToolsConfig{
ToolsUploadPath: "path/to/tools.iso",
}
errs := c.Prepare(interpolate.NewContext())
if len(errs) > 0 {
t.Fatalf("err: %#v", errs)
}
if c.ToolsUploadPath != "path/to/tools.iso" {
t.Fatal("should have used given tools upload path")
}
}
func TestToolsConfigPrepare_ErrorIfOnlySource(t *testing.T) {
c := &ToolsConfig{
ToolsSourcePath: "path/to/tools.iso",
}
errs := c.Prepare(interpolate.NewContext())
if len(errs) != 1 {
t.Fatalf("Should have received an error because the flavor and " +
"upload path aren't set")
}
}
func TestToolsConfigPrepare_SourceSuccess(t *testing.T) {
for _, c := range []*ToolsConfig{
&ToolsConfig{
ToolsSourcePath: "path/to/tools.iso",
ToolsUploadPath: "partypath.iso",
},
&ToolsConfig{
ToolsSourcePath: "path/to/tools.iso",
ToolsUploadFlavor: "linux",
},
} {
errs := c.Prepare(interpolate.NewContext())
if len(errs) != 0 {
t.Fatalf("Should not have received an error")
}
}
}

Some files were not shown because too many files have changed in this diff Show More