Merge remote-tracking branch 'origin/master' into pr/featheredtoast/9626-1
This commit is contained in:
commit
b2be255057
|
@ -7,11 +7,11 @@ version: 2.1
|
|||
executors:
|
||||
golang:
|
||||
docker:
|
||||
- image: circleci/golang:1.13
|
||||
- image: circleci/golang:1.15
|
||||
resource_class: medium+
|
||||
darwin:
|
||||
macos:
|
||||
xcode: "9.0"
|
||||
xcode: "12.0.0"
|
||||
|
||||
commands:
|
||||
install-go-run-tests-unix:
|
||||
|
@ -67,7 +67,7 @@ jobs:
|
|||
steps:
|
||||
- install-go-run-tests-unix:
|
||||
GOOS: darwin
|
||||
GOVERSION: "1.13"
|
||||
GOVERSION: "1.15"
|
||||
- codecov/upload:
|
||||
file: coverage.txt
|
||||
test-windows:
|
||||
|
@ -76,7 +76,7 @@ jobs:
|
|||
shell: bash.exe
|
||||
steps:
|
||||
- install-go-run-tests-windows:
|
||||
GOVERSION: "1.13"
|
||||
GOVERSION: "1.15"
|
||||
- codecov/upload:
|
||||
file: coverage.txt
|
||||
check-lint:
|
||||
|
|
|
@ -13,5 +13,6 @@ coverage:
|
|||
project: off
|
||||
patch: off
|
||||
|
||||
ignore: # ignore hcl2spec generated code for coverage
|
||||
- "**/*.hcl2spec.go"
|
||||
ignore: # ignore hcl2spec generated code for coverage and mocks
|
||||
- "**/*.hcl2spec.go"
|
||||
- "**/*_mock.go"
|
|
@ -6,6 +6,7 @@
|
|||
*.mdx text eol=lf
|
||||
*.ps1 text eol=lf
|
||||
*.hcl text eol=lf
|
||||
*.txt text eol=lf
|
||||
go.mod text eol=lf
|
||||
go.sum text eol=lf
|
||||
common/test-fixtures/root/* eol=lf
|
||||
|
|
114
CHANGELOG.md
114
CHANGELOG.md
|
@ -1,4 +1,116 @@
|
|||
## 1.6.3 (Upcoming)
|
||||
## 1.6.5 (Upcoming)
|
||||
|
||||
### BUG FIXES:
|
||||
|
||||
* core/hcl2: Packer HCL's "Coalesce" function now behaves same way as
|
||||
Terraform's. [GH-10016]
|
||||
* core/HCL: Hide sensitive variables from output. [GH-10031]
|
||||
* core: Fix artifact handling so that input artifacts are properly preserved in
|
||||
postprocessors that don't modify artifacts. [GH-9996]
|
||||
* core: Fix pathing in cd_files to copy proper directory tree when user
|
||||
provided absolute paths. [GH-10022]
|
||||
* provisioner/ansible: Ansible galaxy no longer forces use of collections in v1
|
||||
files. [GH-10010]
|
||||
|
||||
### IMPROVEMENTS:
|
||||
|
||||
* builder/oracle-oci: New option to specify image compartment separate from
|
||||
build compartment. [GH-10040]
|
||||
* builder/oracle-oci: New option to specify boot volume size. [GH-10017]
|
||||
|
||||
## 1.6.4 (September 30, 2020)
|
||||
|
||||
### BUG FIXES:
|
||||
* builder/amazon: Fix authentication issue when using instance profiles or
|
||||
assumed roles for loading session-derived credentials. [GH-10007]
|
||||
* builder/azure: Fix crash when using `azure_tag` or `azure_tags` configuration
|
||||
options. [GH-10014]
|
||||
* builder/qemu: Ensure `qemu_img_args` are honored during the disk convert
|
||||
step. [GH-10001]
|
||||
|
||||
## 1.6.3 (September 25, 2020)
|
||||
|
||||
### 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.
|
||||
[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_directory 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/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]
|
||||
* 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.
|
||||
[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-import: Fix S3 URL construct process. [GH-9931]
|
||||
|
||||
## 1.6.2 (August 28, 2020)
|
||||
|
||||
|
|
|
@ -41,7 +41,9 @@ 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"`
|
||||
|
@ -50,6 +52,7 @@ type FlatConfig struct {
|
|||
RawRegion *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"`
|
||||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||
SkipCredsValidation *bool `mapstructure:"skip_credential_validation" cty:"skip_credential_validation" hcl:"skip_credential_validation"`
|
||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||
|
@ -117,7 +120,9 @@ 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},
|
||||
|
@ -126,6 +131,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
|
||||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||
"skip_credential_validation": &hcldec.AttrSpec{Name: "skip_credential_validation", Type: cty.Bool, Required: false},
|
||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
//go:generate struct-markdown
|
||||
//go:generate mapstructure-to-hcl2 -type VaultAWSEngineOptions
|
||||
//go:generate mapstructure-to-hcl2 -type VaultAWSEngineOptions,AssumeRoleConfig
|
||||
|
||||
package common
|
||||
|
||||
|
@ -11,15 +11,67 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
awsCredentials "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"
|
||||
awsbase "github.com/hashicorp/aws-sdk-go-base"
|
||||
cleanhttp "github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
vaultapi "github.com/hashicorp/vault/api"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
|
@ -48,10 +100,17 @@ 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`
|
||||
|
@ -86,6 +145,8 @@ type AccessConfig struct {
|
|||
// validation of the ami_regions configuration option. Default false.
|
||||
SkipValidation bool `mapstructure:"skip_region_validation" required:"false"`
|
||||
SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"`
|
||||
// Set to true if you want to skip validating AWS credentials before runtime.
|
||||
SkipCredsValidation bool `mapstructure:"skip_credential_validation"`
|
||||
// The access token to use. This is different from the
|
||||
// access key and secret key. If you're not sure what this is, then you
|
||||
// probably don't need it. This will also be read from the AWS_SESSION_TOKEN
|
||||
|
@ -152,16 +213,13 @@ 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)
|
||||
}
|
||||
|
||||
staticCreds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token)
|
||||
if _, err := staticCreds.Get(); err != credentials.ErrStaticCredentialsEmpty {
|
||||
config.WithCredentials(staticCreds)
|
||||
}
|
||||
|
||||
// Set AWS config defaults.
|
||||
if c.RawRegion != "" {
|
||||
config = config.WithRegion(c.RawRegion)
|
||||
}
|
||||
|
@ -179,6 +237,16 @@ 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,
|
||||
|
@ -204,9 +272,7 @@ func (c *AccessConfig) Session() (*session.Session, error) {
|
|||
cp, err := c.session.Config.Credentials.Get()
|
||||
|
||||
if IsAWSErr(err, "NoCredentialProviders", "") {
|
||||
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.")
|
||||
return nil, c.NewNoValidCredentialSourcesError(err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
@ -237,6 +303,42 @@ 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) {
|
||||
// Reload values into the config used by the Packer-Terraform shared SDK
|
||||
awsbaseConfig := &awsbase.Config{
|
||||
AccessKey: c.AccessKey,
|
||||
AssumeRoleARN: c.AssumeRole.AssumeRoleARN,
|
||||
AssumeRoleDurationSeconds: c.AssumeRole.AssumeRoleDurationSeconds,
|
||||
AssumeRoleExternalID: c.AssumeRole.AssumeRoleExternalID,
|
||||
AssumeRolePolicy: c.AssumeRole.AssumeRolePolicy,
|
||||
AssumeRolePolicyARNs: c.AssumeRole.AssumeRolePolicyARNs,
|
||||
AssumeRoleSessionName: c.AssumeRole.AssumeRoleSessionName,
|
||||
AssumeRoleTags: c.AssumeRole.AssumeRoleTags,
|
||||
AssumeRoleTransitiveTagKeys: c.AssumeRole.AssumeRoleTransitiveTagKeys,
|
||||
CredsFilename: c.CredsFilename,
|
||||
DebugLogging: false,
|
||||
// TODO: implement for Packer
|
||||
// IamEndpoint: c.Endpoints["iam"],
|
||||
Insecure: c.InsecureSkipTLSVerify,
|
||||
MaxRetries: c.MaxRetries,
|
||||
Profile: c.ProfileName,
|
||||
Region: c.RawRegion,
|
||||
SecretKey: c.SecretKey,
|
||||
SkipCredsValidation: c.SkipCredsValidation,
|
||||
SkipMetadataApiCheck: c.SkipMetadataApiCheck,
|
||||
// TODO: implement for Packer
|
||||
// SkipRequestingAccountId: c.SkipRequestingAccountId,
|
||||
// StsEndpoint: c.Endpoints["sts"],
|
||||
Token: c.Token,
|
||||
}
|
||||
|
||||
return awsbase.GetCredentials(awsbaseConfig)
|
||||
}
|
||||
|
||||
func (c *AccessConfig) GetCredsFromVault() error {
|
||||
// const EnvVaultAddress = "VAULT_ADDR"
|
||||
// const EnvVaultToken = "VAULT_TOKEN"
|
||||
|
@ -306,6 +408,13 @@ 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"; DO NOT EDIT.
|
||||
// Code generated by "mapstructure-to-hcl2 -type VaultAWSEngineOptions,AssumeRoleConfig"; DO NOT EDIT.
|
||||
package common
|
||||
|
||||
import (
|
||||
|
@ -6,6 +6,43 @@ 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 {
|
||||
|
|
|
@ -473,6 +473,13 @@ 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`.
|
||||
|
@ -535,6 +542,10 @@ 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 != "" {
|
||||
|
|
|
@ -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 with following params %v", aws.StringValue(input.Target), input.Parameters)
|
||||
log.Printf("Starting PortForwarding session to instance %q", aws.StringValue(input.Target))
|
||||
|
||||
var output *ssm.StartSessionOutput
|
||||
err := retry.Config{
|
||||
|
@ -110,15 +110,30 @@ func (d *SSMDriver) openTunnelForSession(ctx context.Context) error {
|
|||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case output := <-stderrCh:
|
||||
case output, ok := <-stderrCh:
|
||||
if !ok {
|
||||
stderrCh = nil
|
||||
break
|
||||
}
|
||||
|
||||
if output != "" {
|
||||
log.Printf("[ERROR] %s: %s", prefix, output)
|
||||
}
|
||||
case output := <-stdoutCh:
|
||||
case output, ok := <-stdoutCh:
|
||||
if !ok {
|
||||
stdoutCh = nil
|
||||
break
|
||||
}
|
||||
|
||||
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,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
|
@ -21,6 +22,7 @@ type StepCreateSSMTunnel struct {
|
|||
RemotePortNumber int
|
||||
SSMAgentEnabled bool
|
||||
instanceId string
|
||||
PauseBeforeSSM time.Duration
|
||||
driver *SSMDriver
|
||||
}
|
||||
|
||||
|
@ -32,6 +34,17 @@ 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)
|
||||
|
|
|
@ -10,7 +10,6 @@ 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"
|
||||
|
@ -281,7 +280,6 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
|
|||
}
|
||||
|
||||
var createOutput *ec2.CreateFleetOutput
|
||||
|
||||
err = retry.Config{
|
||||
Tries: 11,
|
||||
ShouldRetry: func(err error) bool {
|
||||
|
@ -291,40 +289,39 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
|
|||
// we can wait on those operations, this can be removed.
|
||||
return true
|
||||
}
|
||||
return request.IsErrorRetryable(err)
|
||||
return false
|
||||
},
|
||||
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())
|
||||
|
|
|
@ -269,6 +269,7 @@ 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,7 +19,9 @@ 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"`
|
||||
|
@ -29,6 +31,7 @@ type FlatConfig struct {
|
|||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
SkipValidation *bool `mapstructure:"skip_region_validation" required:"false" cty:"skip_region_validation" hcl:"skip_region_validation"`
|
||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||
SkipCredsValidation *bool `mapstructure:"skip_credential_validation" cty:"skip_credential_validation" hcl:"skip_credential_validation"`
|
||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||
|
@ -132,6 +135,7 @@ 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"`
|
||||
|
@ -160,7 +164,9 @@ 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},
|
||||
|
@ -170,6 +176,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"skip_region_validation": &hcldec.AttrSpec{Name: "skip_region_validation", Type: cty.Bool, Required: false},
|
||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||
"skip_credential_validation": &hcldec.AttrSpec{Name: "skip_credential_validation", Type: cty.Bool, Required: false},
|
||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||
|
@ -273,6 +280,7 @@ 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,9 +3,6 @@
|
|||
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"
|
||||
|
@ -41,50 +38,6 @@ 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 {
|
||||
|
|
|
@ -293,6 +293,7 @@ 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,7 +62,9 @@ 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"`
|
||||
|
@ -72,6 +74,7 @@ type FlatConfig struct {
|
|||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
SkipValidation *bool `mapstructure:"skip_region_validation" required:"false" cty:"skip_region_validation" hcl:"skip_region_validation"`
|
||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||
SkipCredsValidation *bool `mapstructure:"skip_credential_validation" cty:"skip_credential_validation" hcl:"skip_credential_validation"`
|
||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||
|
@ -154,6 +157,7 @@ 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"`
|
||||
|
@ -204,7 +208,9 @@ 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},
|
||||
|
@ -214,6 +220,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"skip_region_validation": &hcldec.AttrSpec{Name: "skip_region_validation", Type: cty.Bool, Required: false},
|
||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||
"skip_credential_validation": &hcldec.AttrSpec{Name: "skip_credential_validation", Type: cty.Bool, Required: false},
|
||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||
|
@ -296,6 +303,7 @@ 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},
|
||||
|
|
|
@ -263,6 +263,7 @@ 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,7 +64,9 @@ 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"`
|
||||
|
@ -74,6 +76,7 @@ type FlatConfig struct {
|
|||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
SkipValidation *bool `mapstructure:"skip_region_validation" required:"false" cty:"skip_region_validation" hcl:"skip_region_validation"`
|
||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||
SkipCredsValidation *bool `mapstructure:"skip_credential_validation" cty:"skip_credential_validation" hcl:"skip_credential_validation"`
|
||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||
|
@ -156,6 +159,7 @@ 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"`
|
||||
|
@ -184,7 +188,9 @@ 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},
|
||||
|
@ -194,6 +200,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"skip_region_validation": &hcldec.AttrSpec{Name: "skip_region_validation", Type: cty.Bool, Required: false},
|
||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||
"skip_credential_validation": &hcldec.AttrSpec{Name: "skip_credential_validation", Type: cty.Bool, Required: false},
|
||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||
|
@ -276,6 +283,7 @@ 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},
|
||||
|
|
|
@ -343,6 +343,7 @@ 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,7 +19,9 @@ 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"`
|
||||
|
@ -29,6 +31,7 @@ type FlatConfig struct {
|
|||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
SkipValidation *bool `mapstructure:"skip_region_validation" required:"false" cty:"skip_region_validation" hcl:"skip_region_validation"`
|
||||
SkipMetadataApiCheck *bool `mapstructure:"skip_metadata_api_check" cty:"skip_metadata_api_check" hcl:"skip_metadata_api_check"`
|
||||
SkipCredsValidation *bool `mapstructure:"skip_credential_validation" cty:"skip_credential_validation" hcl:"skip_credential_validation"`
|
||||
Token *string `mapstructure:"token" required:"false" cty:"token" hcl:"token"`
|
||||
VaultAWSEngine *common.FlatVaultAWSEngineOptions `mapstructure:"vault_aws_engine" required:"false" cty:"vault_aws_engine" hcl:"vault_aws_engine"`
|
||||
PollingConfig *common.FlatAWSPollingConfig `mapstructure:"aws_polling" required:"false" cty:"aws_polling" hcl:"aws_polling"`
|
||||
|
@ -132,6 +135,7 @@ 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"`
|
||||
|
@ -166,7 +170,9 @@ 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},
|
||||
|
@ -176,6 +182,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"skip_region_validation": &hcldec.AttrSpec{Name: "skip_region_validation", Type: cty.Bool, Required: false},
|
||||
"skip_metadata_api_check": &hcldec.AttrSpec{Name: "skip_metadata_api_check", Type: cty.Bool, Required: false},
|
||||
"skip_credential_validation": &hcldec.AttrSpec{Name: "skip_credential_validation", Type: cty.Bool, Required: false},
|
||||
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||
"vault_aws_engine": &hcldec.BlockSpec{TypeName: "vault_aws_engine", Nested: hcldec.ObjectSpec((*common.FlatVaultAWSEngineOptions)(nil).HCL2Spec())},
|
||||
"aws_polling": &hcldec.BlockSpec{TypeName: "aws_polling", Nested: hcldec.ObjectSpec((*common.FlatAWSPollingConfig)(nil).HCL2Spec())},
|
||||
|
@ -279,6 +286,7 @@ 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())},
|
||||
|
|
|
@ -390,7 +390,7 @@ func (b *Builder) getBlobAccount(ctx context.Context, client *AzureClient, resou
|
|||
func (b *Builder) configureStateBag(stateBag multistep.StateBag) {
|
||||
stateBag.Put(constants.AuthorizedKey, b.config.sshAuthorizedKey)
|
||||
|
||||
stateBag.Put(constants.ArmTags, b.config.AzureTags)
|
||||
stateBag.Put(constants.ArmTags, packerAzureCommon.MapToAzureTags(b.config.AzureTags))
|
||||
stateBag.Put(constants.ArmComputeName, b.config.tmpComputeName)
|
||||
stateBag.Put(constants.ArmDeploymentName, b.config.tmpDeploymentName)
|
||||
|
||||
|
|
|
@ -223,7 +223,11 @@ const testBuilderAccManagedDiskLinux = `
|
|||
"image_sku": "16.04-LTS",
|
||||
|
||||
"location": "South Central US",
|
||||
"vm_size": "Standard_DS2_v2"
|
||||
"vm_size": "Standard_DS2_v2",
|
||||
"azure_tags": {
|
||||
"env": "testing",
|
||||
"builder": "packer"
|
||||
}
|
||||
}]
|
||||
}
|
||||
`
|
||||
|
|
|
@ -33,3 +33,35 @@ func TestStateBagShouldBePopulatedExpectedValues(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStateBagShouldPoluateExpectedTags(t *testing.T) {
|
||||
var testSubject Builder
|
||||
|
||||
expectedTags := map[string]string{
|
||||
"env": "test",
|
||||
"builder": "packer",
|
||||
}
|
||||
armConfig := getArmBuilderConfiguration()
|
||||
armConfig["azure_tags"] = expectedTags
|
||||
|
||||
_, _, err := testSubject.Prepare(armConfig, getPackerConfiguration())
|
||||
if err != nil {
|
||||
t.Fatalf("failed to prepare: %s", err)
|
||||
}
|
||||
|
||||
tags, ok := testSubject.stateBag.Get(constants.ArmTags).(map[string]*string)
|
||||
if !ok {
|
||||
t.Errorf("Expected the builder's state bag to contain tags of type %T, but didn't.", testSubject.config.AzureTags)
|
||||
}
|
||||
|
||||
if len(tags) != len(expectedTags) {
|
||||
t.Errorf("expect tags from state to be the same length as tags from config")
|
||||
}
|
||||
|
||||
for k, v := range tags {
|
||||
if expectedTags[k] != *v {
|
||||
t.Errorf("expect tag value of %s to be %s, but got %s", k, expectedTags[k], *v)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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: c.AzureTags,
|
||||
Tags: azcommon.MapToAzureTags(c.AzureTags),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -596,10 +596,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
// copy singular blocks
|
||||
for _, kv := range c.AzureTag {
|
||||
v := kv.Value
|
||||
c.AzureTags[kv.Name] = &v
|
||||
}
|
||||
c.AzureTag.CopyOn(&c.AzureTags)
|
||||
|
||||
err = c.ClientConfig.SetDefaultValues()
|
||||
if err != nil {
|
||||
|
@ -807,8 +804,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)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1063,13 +1060,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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -6,7 +6,9 @@ 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.
|
||||
|
@ -910,32 +912,55 @@ func TestConfigShouldAcceptTags(t *testing.T) {
|
|||
},
|
||||
}
|
||||
|
||||
var c Config
|
||||
c := Config{
|
||||
AzureTag: hcl2template.NameValues{
|
||||
{Name: "tag03", Value: "value03"},
|
||||
},
|
||||
}
|
||||
_, err := c.Prepare(config, getPackerConfiguration())
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(c.AzureTags) != 2 {
|
||||
t.Fatalf("expected to find 2 tags, but got %d", len(c.AzureTags))
|
||||
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 _, ok := c.AzureTags["tag01"]; !ok {
|
||||
t.Error("expected to find key=\"tag01\", but did not")
|
||||
c := Config{
|
||||
AzureTag: hcl2template.NameValues{
|
||||
{Name: "tag03", Value: "value03"},
|
||||
},
|
||||
}
|
||||
if _, ok := c.AzureTags["tag02"]; !ok {
|
||||
t.Error("expected to find key=\"tag02\", but did not")
|
||||
_, err := c.Prepare(config, getPackerConfiguration())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
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)
|
||||
if diff := cmp.Diff(c.AzureTags, map[string]string{
|
||||
"tag03": "value03",
|
||||
}); diff != "" {
|
||||
t.Fatalf("unexpected azure tags: %s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2047,8 +2072,8 @@ func TestConfig_PrepareProvidedWinRMPassword(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func getArmBuilderConfiguration() map[string]string {
|
||||
m := make(map[string]string)
|
||||
func getArmBuilderConfiguration() map[string]interface{} {
|
||||
m := make(map[string]interface{})
|
||||
for _, v := range requiredConfigValues {
|
||||
m[v] = "ignored00"
|
||||
}
|
||||
|
|
|
@ -61,7 +61,13 @@ func (s *StepCreateResourceGroup) Run(ctx context.Context, state multistep.State
|
|||
|
||||
var resourceGroupName = state.Get(constants.ArmResourceGroupName).(string)
|
||||
var location = state.Get(constants.ArmLocation).(string)
|
||||
var tags = state.Get(constants.ArmTags).(map[string]*string)
|
||||
tags, ok := state.Get(constants.ArmTags).(map[string]*string)
|
||||
if !ok {
|
||||
err := fmt.Errorf("failed to extract tags from state bag")
|
||||
state.Put(constants.Error, err)
|
||||
s.error(err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
exists, err := s.exists(ctx, resourceGroupName)
|
||||
if err != nil {
|
||||
|
|
|
@ -220,3 +220,32 @@ func createTestExistingStateBagStepCreateResourceGroup() multistep.StateBag {
|
|||
stateBag.Put(constants.ArmTags, tags)
|
||||
return stateBag
|
||||
}
|
||||
|
||||
func TestStepCreateResourceGroupShouldFailIfTagsFailCast(t *testing.T) {
|
||||
stateBag := new(multistep.BasicStateBag)
|
||||
|
||||
stateBag.Put(constants.ArmLocation, "Unit Test: Location")
|
||||
stateBag.Put(constants.ArmResourceGroupName, "Unit Test: ResourceGroupName")
|
||||
stateBag.Put(constants.ArmIsExistingResourceGroup, true)
|
||||
|
||||
value := "Unit Test: Tags"
|
||||
tags := map[string]string{
|
||||
"tag01": value,
|
||||
}
|
||||
|
||||
stateBag.Put(constants.ArmTags, tags)
|
||||
var testSubject = &StepCreateResourceGroup{
|
||||
create: func(context.Context, string, string, map[string]*string) error { return nil },
|
||||
say: func(message string) {},
|
||||
error: func(e error) {},
|
||||
exists: func(context.Context, string) (bool, error) { return false, nil },
|
||||
}
|
||||
var result = testSubject.Run(context.Background(), stateBag)
|
||||
if result != multistep.ActionHalt {
|
||||
t.Fatalf("Expected the step to return 'ActionHalt', but got '%d'.", result)
|
||||
}
|
||||
|
||||
if _, ok := stateBag.GetOk(constants.Error); ok == false {
|
||||
t.Fatalf("Expected the step to set stateBag['%s'], but it was not.", constants.Error)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ type FlatConfig struct {
|
|||
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
|
||||
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
|
||||
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
|
||||
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
|
||||
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
|
||||
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
|
||||
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
|
||||
|
@ -132,6 +133,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
|
||||
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
|
||||
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
|
||||
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
|
||||
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
|
||||
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
|
||||
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
|
||||
|
|
|
@ -30,19 +30,22 @@ func (a *ImportArtifact) Id() string {
|
|||
}
|
||||
|
||||
func (a *ImportArtifact) String() string {
|
||||
tags := a.StateData["docker_tags"]
|
||||
if tags == nil {
|
||||
return fmt.Sprintf("Imported Docker image: %s", a.Id())
|
||||
}
|
||||
cast := tags.([]interface{})
|
||||
names := []string{}
|
||||
for _, name := range cast {
|
||||
if n, ok := name.(string); ok {
|
||||
names = append(names, n)
|
||||
var tags []string
|
||||
switch t := a.StateData["docker_tags"].(type) {
|
||||
case []string:
|
||||
tags = t
|
||||
case []interface{}:
|
||||
for _, name := range t {
|
||||
if n, ok := name.(string); ok {
|
||||
tags = append(tags, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("Imported Docker image: %s with tags %s",
|
||||
a.Id(), strings.Join(names, " "))
|
||||
if len(tags) > 0 {
|
||||
return fmt.Sprintf("Imported Docker image: %s with tags %s",
|
||||
a.Id(), strings.Join(tags, " "))
|
||||
}
|
||||
return fmt.Sprintf("Imported Docker image: %s", a.Id())
|
||||
}
|
||||
|
||||
func (a *ImportArtifact) State(name string) interface{} {
|
||||
|
|
|
@ -5,6 +5,7 @@ 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"
|
||||
|
@ -29,7 +30,9 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||
return nil, warnings, errs
|
||||
}
|
||||
|
||||
return nil, warnings, nil
|
||||
return []string{
|
||||
"ImageSha256",
|
||||
}, warnings, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||
|
@ -44,6 +47,16 @@ 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{},
|
||||
|
@ -67,7 +80,11 @@ 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))
|
||||
steps = append(steps,
|
||||
new(StepCommit),
|
||||
&StepSetGeneratedData{ // Adds ImageSha256 variable available after StepCommit
|
||||
GeneratedData: generatedData,
|
||||
})
|
||||
} else if b.config.ExportPath != "" {
|
||||
log.Printf("[DEBUG] Container will be exported to %s", b.config.ExportPath)
|
||||
steps = append(steps, new(StepExport))
|
||||
|
@ -75,15 +92,6 @@ 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)
|
||||
|
|
|
@ -26,6 +26,9 @@ 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
|
||||
|
|
|
@ -159,6 +159,23 @@ 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()
|
||||
|
||||
|
|
|
@ -28,6 +28,11 @@ type MockDriver struct {
|
|||
IPAddressResult string
|
||||
IPAddressErr error
|
||||
|
||||
Sha256Called bool
|
||||
Sha256Id string
|
||||
Sha256Result string
|
||||
Sha256Err error
|
||||
|
||||
KillCalled bool
|
||||
KillID string
|
||||
KillError error
|
||||
|
@ -118,6 +123,12 @@ 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
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
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...
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
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)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ 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"
|
||||
|
@ -124,12 +125,12 @@ func NewDriverGCE(ui packer.Ui, p string, conf *jwt.Config, vaultOauth string) (
|
|||
}
|
||||
|
||||
log.Printf("[INFO] Instantiating GCE client...")
|
||||
service, err := compute.New(client)
|
||||
service, err := compute.NewService(context.TODO(), option.WithHTTPClient(client))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
osLoginService, err := oslogin.New(client)
|
||||
osLoginService, err := oslogin.NewService(context.TODO(), option.WithHTTPClient(client))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -37,6 +37,7 @@ const (
|
|||
|
||||
type CommonConfig struct {
|
||||
common.FloppyConfig `mapstructure:",squash"`
|
||||
common.CDConfig `mapstructure:",squash"`
|
||||
// The block size of the VHD to be created.
|
||||
// Recommended disk block size for Linux hyper-v guests is 1 MiB. This
|
||||
// defaults to "32" MiB.
|
||||
|
@ -210,8 +211,8 @@ func (c *CommonConfig) Prepare(ctx *interpolate.Context, pc *common.PackerConfig
|
|||
}
|
||||
|
||||
// Errors
|
||||
floppyerrs := c.FloppyConfig.Prepare(ctx)
|
||||
errs = append(errs, floppyerrs...)
|
||||
errs = append(errs, c.FloppyConfig.Prepare(ctx)...)
|
||||
errs = append(errs, c.CDConfig.Prepare(ctx)...)
|
||||
if c.GuestAdditionsMode == "" {
|
||||
if c.GuestAdditionsPath != "" {
|
||||
c.GuestAdditionsMode = "attach"
|
||||
|
|
|
@ -34,7 +34,17 @@ func (s *StepMountSecondaryDvdImages) Run(ctx context.Context, state multistep.S
|
|||
// For IDE, there are only 2 controllers (0,1) with 2 locations each (0,1)
|
||||
var dvdProperties []DvdControllerProperties
|
||||
|
||||
for _, isoPath := range s.IsoPaths {
|
||||
isoPaths := s.IsoPaths
|
||||
|
||||
// Add our custom CD, if it exists
|
||||
cd_path, ok := state.Get("cd_path").(string)
|
||||
if ok {
|
||||
if cd_path != "" {
|
||||
isoPaths = append(isoPaths, cd_path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, isoPath := range isoPaths {
|
||||
var properties DvdControllerProperties
|
||||
|
||||
controllerNumber, controllerLocation, err := driver.CreateDvdDrive(vmName, isoPath, s.Generation)
|
||||
|
|
|
@ -254,7 +254,10 @@ 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,
|
||||
|
|
|
@ -20,6 +20,7 @@ type FlatConfig struct {
|
|||
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
|
||||
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
|
||||
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
|
||||
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
|
||||
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
|
||||
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
|
||||
|
@ -79,6 +80,8 @@ 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"`
|
||||
DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"`
|
||||
RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
|
||||
SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"`
|
||||
|
@ -136,6 +139,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
|
||||
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
|
||||
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
|
||||
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
|
||||
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
|
||||
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
|
||||
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
|
||||
|
@ -195,6 +199,8 @@ 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},
|
||||
"disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false},
|
||||
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
|
||||
"secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false},
|
||||
|
|
|
@ -294,7 +294,10 @@ 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,
|
||||
|
|
|
@ -20,6 +20,7 @@ type FlatConfig struct {
|
|||
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
|
||||
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
|
||||
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
|
||||
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
|
||||
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
|
||||
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
|
||||
|
@ -79,6 +80,8 @@ 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"`
|
||||
DiskBlockSize *uint `mapstructure:"disk_block_size" required:"false" cty:"disk_block_size" hcl:"disk_block_size"`
|
||||
RamSize *uint `mapstructure:"memory" required:"false" cty:"memory" hcl:"memory"`
|
||||
SecondaryDvdImages []string `mapstructure:"secondary_iso_images" required:"false" cty:"secondary_iso_images" hcl:"secondary_iso_images"`
|
||||
|
@ -138,6 +141,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
|
||||
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
|
||||
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
|
||||
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
|
||||
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
|
||||
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
|
||||
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
|
||||
|
@ -197,6 +201,8 @@ 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},
|
||||
"disk_block_size": &hcldec.AttrSpec{Name: "disk_block_size", Type: cty.Number, Required: false},
|
||||
"memory": &hcldec.AttrSpec{Name: "memory", Type: cty.Number, Required: false},
|
||||
"secondary_iso_images": &hcldec.AttrSpec{Name: "secondary_iso_images", Type: cty.List(cty.String), Required: false},
|
||||
|
|
|
@ -3,16 +3,20 @@
|
|||
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"
|
||||
)
|
||||
|
||||
|
@ -236,6 +240,15 @@ 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,
|
||||
|
@ -273,3 +286,49 @@ 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))
|
||||
}
|
||||
|
|
|
@ -71,6 +71,10 @@ 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)
|
||||
|
@ -98,11 +102,13 @@ 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,
|
||||
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,
|
||||
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,
|
||||
},
|
||||
&StepDiscoverNetwork{
|
||||
Networks: b.config.Networks,
|
||||
|
|
|
@ -95,6 +95,8 @@ 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"`
|
||||
|
@ -220,6 +222,8 @@ 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},
|
||||
|
|
|
@ -33,6 +33,11 @@ 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
|
||||
|
@ -247,10 +252,19 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
}
|
||||
}
|
||||
|
||||
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."))
|
||||
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.Flavor == "" {
|
||||
|
@ -283,9 +297,9 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
|
|||
}
|
||||
}
|
||||
|
||||
// if neither ID or image name is provided outside the filter, build the
|
||||
// filter
|
||||
if len(c.SourceImage) == 0 && len(c.SourceImageName) == 0 {
|
||||
// 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 {
|
||||
|
||||
listOpts, filterErr := c.SourceImageFilters.Filters.Build()
|
||||
|
||||
|
@ -295,6 +309,11 @@ 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
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ package openstack
|
|||
|
||||
import (
|
||||
"os"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
|
||||
|
@ -132,6 +133,49 @@ 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) {
|
||||
|
|
|
@ -4,7 +4,9 @@ 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"
|
||||
|
@ -12,11 +14,13 @@ import (
|
|||
)
|
||||
|
||||
type StepSourceImageInfo struct {
|
||||
SourceImage string
|
||||
SourceImageName string
|
||||
SourceImageOpts images.ListOpts
|
||||
SourceMostRecent bool
|
||||
SourceProperties map[string]string
|
||||
SourceImage string
|
||||
SourceImageName string
|
||||
ExternalSourceImageURL string
|
||||
ExternalSourceImageFormat string
|
||||
SourceImageOpts images.ListOpts
|
||||
SourceMostRecent bool
|
||||
SourceProperties map[string]string
|
||||
}
|
||||
|
||||
func PropertiesSatisfied(image *images.Image, props *map[string]string) bool {
|
||||
|
@ -33,12 +37,6 @@ 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)
|
||||
|
@ -47,6 +45,70 @@ 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,
|
||||
|
@ -117,5 +179,25 @@ func (s *StepSourceImageInfo) Run(ctx context.Context, state multistep.StateBag)
|
|||
}
|
||||
|
||||
func (s *StepSourceImageInfo) Cleanup(state multistep.StateBag) {
|
||||
// No cleanup required for backout
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
//go:generate mapstructure-to-hcl2 -type Config
|
||||
//go:generate mapstructure-to-hcl2 -type Config,CreateVNICDetails
|
||||
|
||||
package oci
|
||||
|
||||
|
@ -22,6 +22,19 @@ import (
|
|||
ociauth "github.com/oracle/oci-go-sdk/common/auth"
|
||||
)
|
||||
|
||||
type CreateVNICDetails struct {
|
||||
// fields that can be specified under "create_vnic_details"
|
||||
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false"`
|
||||
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false"`
|
||||
DisplayName *string `mapstructure:"display_name" required:"false"`
|
||||
FreeformTags map[string]string `mapstructure:"tags" required:"false"`
|
||||
HostnameLabel *string `mapstructure:"hostname_label" required:"false"`
|
||||
NsgIds []string `mapstructure:"nsg_ids" required:"false"`
|
||||
PrivateIp *string `mapstructure:"private_ip" required:"false"`
|
||||
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false"`
|
||||
SubnetId *string `mapstructure:"subnet_id" required:"false"`
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
|
@ -56,14 +69,16 @@ type Config struct {
|
|||
CompartmentID string `mapstructure:"compartment_ocid"`
|
||||
|
||||
// Image
|
||||
BaseImageID string `mapstructure:"base_image_ocid"`
|
||||
Shape string `mapstructure:"shape"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
BaseImageID string `mapstructure:"base_image_ocid"`
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
ImageCompartmentID string `mapstructure:"image_compartment_ocid"`
|
||||
|
||||
// Instance
|
||||
InstanceName string `mapstructure:"instance_name"`
|
||||
InstanceTags map[string]string `mapstructure:"instance_tags"`
|
||||
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags"`
|
||||
Shape string `mapstructure:"shape"`
|
||||
BootVolumeSizeInGBs int64 `mapstructure:"disk_size"`
|
||||
|
||||
// Metadata optionally contains custom metadata key/value pairs provided in the
|
||||
// configuration. While this can be used to set metadata["user_data"] the explicit
|
||||
|
@ -77,7 +92,8 @@ type Config struct {
|
|||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
|
||||
// Networking
|
||||
SubnetID string `mapstructure:"subnet_ocid"`
|
||||
SubnetID string `mapstructure:"subnet_ocid"`
|
||||
CreateVnicDetails CreateVNICDetails `mapstructure:"create_vnic_details"`
|
||||
|
||||
// Tagging
|
||||
Tags map[string]string `mapstructure:"tags"`
|
||||
|
@ -239,6 +255,10 @@ func (c *Config) Prepare(raws ...interface{}) error {
|
|||
c.CompartmentID = tenancyOCID
|
||||
}
|
||||
|
||||
if c.ImageCompartmentID == "" {
|
||||
c.ImageCompartmentID = c.CompartmentID
|
||||
}
|
||||
|
||||
if c.Shape == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'shape' must be specified"))
|
||||
|
@ -312,6 +332,15 @@ func (c *Config) Prepare(raws ...interface{}) error {
|
|||
}
|
||||
}
|
||||
|
||||
// Set default boot volume size to 50 if not set
|
||||
// Check if size set is allowed by OCI
|
||||
if c.BootVolumeSizeInGBs == 0 {
|
||||
c.BootVolumeSizeInGBs = 50
|
||||
} else if c.BootVolumeSizeInGBs < 50 || c.BootVolumeSizeInGBs > 16384 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("'disk_size' must be between 50 and 16384 GBs"))
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
|
||||
// Code generated by "mapstructure-to-hcl2 -type Config,CreateVNICDetails"; DO NOT EDIT.
|
||||
package oci
|
||||
|
||||
import (
|
||||
|
@ -76,15 +76,18 @@ type FlatConfig struct {
|
|||
AvailabilityDomain *string `mapstructure:"availability_domain" cty:"availability_domain" hcl:"availability_domain"`
|
||||
CompartmentID *string `mapstructure:"compartment_ocid" cty:"compartment_ocid" hcl:"compartment_ocid"`
|
||||
BaseImageID *string `mapstructure:"base_image_ocid" cty:"base_image_ocid" hcl:"base_image_ocid"`
|
||||
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
|
||||
ImageCompartmentID *string `mapstructure:"image_compartment_ocid" cty:"image_compartment_ocid" hcl:"image_compartment_ocid"`
|
||||
InstanceName *string `mapstructure:"instance_name" cty:"instance_name" hcl:"instance_name"`
|
||||
InstanceTags map[string]string `mapstructure:"instance_tags" cty:"instance_tags" hcl:"instance_tags"`
|
||||
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags" cty:"instance_defined_tags" hcl:"instance_defined_tags"`
|
||||
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||
BootVolumeSizeInGBs *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
|
||||
Metadata map[string]string `mapstructure:"metadata" cty:"metadata" hcl:"metadata"`
|
||||
UserData *string `mapstructure:"user_data" cty:"user_data" hcl:"user_data"`
|
||||
UserDataFile *string `mapstructure:"user_data_file" cty:"user_data_file" hcl:"user_data_file"`
|
||||
SubnetID *string `mapstructure:"subnet_ocid" cty:"subnet_ocid" hcl:"subnet_ocid"`
|
||||
CreateVnicDetails *FlatCreateVNICDetails `mapstructure:"create_vnic_details" cty:"create_vnic_details" hcl:"create_vnic_details"`
|
||||
Tags map[string]string `mapstructure:"tags" cty:"tags" hcl:"tags"`
|
||||
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" cty:"defined_tags" hcl:"defined_tags"`
|
||||
}
|
||||
|
@ -168,17 +171,59 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"availability_domain": &hcldec.AttrSpec{Name: "availability_domain", Type: cty.String, Required: false},
|
||||
"compartment_ocid": &hcldec.AttrSpec{Name: "compartment_ocid", Type: cty.String, Required: false},
|
||||
"base_image_ocid": &hcldec.AttrSpec{Name: "base_image_ocid", Type: cty.String, Required: false},
|
||||
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
|
||||
"image_compartment_ocid": &hcldec.AttrSpec{Name: "image_compartment_ocid", Type: cty.String, Required: false},
|
||||
"instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false},
|
||||
"instance_tags": &hcldec.AttrSpec{Name: "instance_tags", Type: cty.Map(cty.String), Required: false},
|
||||
"instance_defined_tags": &hcldec.AttrSpec{Name: "instance_defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
|
||||
"metadata": &hcldec.AttrSpec{Name: "metadata", Type: cty.Map(cty.String), Required: false},
|
||||
"user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false},
|
||||
"user_data_file": &hcldec.AttrSpec{Name: "user_data_file", Type: cty.String, Required: false},
|
||||
"subnet_ocid": &hcldec.AttrSpec{Name: "subnet_ocid", Type: cty.String, Required: false},
|
||||
"create_vnic_details": &hcldec.BlockSpec{TypeName: "create_vnic_details", Nested: hcldec.ObjectSpec((*FlatCreateVNICDetails)(nil).HCL2Spec())},
|
||||
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
|
||||
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
|
||||
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||
type FlatCreateVNICDetails struct {
|
||||
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false" cty:"assign_public_ip" hcl:"assign_public_ip"`
|
||||
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false" cty:"defined_tags" hcl:"defined_tags"`
|
||||
DisplayName *string `mapstructure:"display_name" required:"false" cty:"display_name" hcl:"display_name"`
|
||||
FreeformTags map[string]string `mapstructure:"tags" required:"false" cty:"tags" hcl:"tags"`
|
||||
HostnameLabel *string `mapstructure:"hostname_label" required:"false" cty:"hostname_label" hcl:"hostname_label"`
|
||||
NsgIds []string `mapstructure:"nsg_ids" required:"false" cty:"nsg_ids" hcl:"nsg_ids"`
|
||||
PrivateIp *string `mapstructure:"private_ip" required:"false" cty:"private_ip" hcl:"private_ip"`
|
||||
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false" cty:"skip_source_dest_check" hcl:"skip_source_dest_check"`
|
||||
SubnetId *string `mapstructure:"subnet_id" required:"false" cty:"subnet_id" hcl:"subnet_id"`
|
||||
}
|
||||
|
||||
// FlatMapstructure returns a new FlatCreateVNICDetails.
|
||||
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
|
||||
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||
func (*CreateVNICDetails) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||
return new(FlatCreateVNICDetails)
|
||||
}
|
||||
|
||||
// HCL2Spec returns the hcl spec of a CreateVNICDetails.
|
||||
// This spec is used by HCL to read the fields of CreateVNICDetails.
|
||||
// The decoded values from this spec will then be applied to a FlatCreateVNICDetails.
|
||||
func (*FlatCreateVNICDetails) HCL2Spec() map[string]hcldec.Spec {
|
||||
s := map[string]hcldec.Spec{
|
||||
"assign_public_ip": &hcldec.AttrSpec{Name: "assign_public_ip", Type: cty.Bool, Required: false},
|
||||
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||
"display_name": &hcldec.AttrSpec{Name: "display_name", Type: cty.String, Required: false},
|
||||
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
|
||||
"hostname_label": &hcldec.AttrSpec{Name: "hostname_label", Type: cty.String, Required: false},
|
||||
"nsg_ids": &hcldec.AttrSpec{Name: "nsg_ids", Type: cty.List(cty.String), Required: false},
|
||||
"private_ip": &hcldec.AttrSpec{Name: "private_ip", Type: cty.String, Required: false},
|
||||
"skip_source_dest_check": &hcldec.AttrSpec{Name: "skip_source_dest_check", Type: cty.Bool, Required: false},
|
||||
"subnet_id": &hcldec.AttrSpec{Name: "subnet_id", Type: cty.String, Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
|
|||
|
||||
// Image
|
||||
"base_image_ocid": "ocd1...",
|
||||
"shape": "VM.Standard1.1",
|
||||
"image_name": "HelloWorld",
|
||||
|
||||
// Networking
|
||||
|
@ -36,6 +35,17 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
|
|||
"defined_tags": map[string]map[string]interface{}{
|
||||
"namespace": {"key": "value"},
|
||||
},
|
||||
|
||||
// Instance Details
|
||||
"instance_name": "hello-world",
|
||||
"instance_tags": map[string]string{
|
||||
"key": "value",
|
||||
},
|
||||
"create_vnic_details": map[string]interface{}{
|
||||
"nsg_ids": []string{"ocd1..."},
|
||||
},
|
||||
"shape": "VM.Standard1.1",
|
||||
"disk_size": 60,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,6 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
|
|||
CompartmentId: &d.cfg.CompartmentID,
|
||||
DefinedTags: d.cfg.InstanceDefinedTags,
|
||||
FreeformTags: d.cfg.InstanceTags,
|
||||
ImageId: &d.cfg.BaseImageID,
|
||||
Shape: &d.cfg.Shape,
|
||||
SubnetId: &d.cfg.SubnetID,
|
||||
Metadata: metadata,
|
||||
|
@ -67,6 +66,27 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
|
|||
instanceDetails.DisplayName = &d.cfg.InstanceName
|
||||
}
|
||||
|
||||
// Pass VNIC details, if specified, to the instance
|
||||
CreateVnicDetails := core.CreateVnicDetails{
|
||||
AssignPublicIp: d.cfg.CreateVnicDetails.AssignPublicIp,
|
||||
DisplayName: d.cfg.CreateVnicDetails.DisplayName,
|
||||
HostnameLabel: d.cfg.CreateVnicDetails.HostnameLabel,
|
||||
NsgIds: d.cfg.CreateVnicDetails.NsgIds,
|
||||
PrivateIp: d.cfg.CreateVnicDetails.PrivateIp,
|
||||
SkipSourceDestCheck: d.cfg.CreateVnicDetails.SkipSourceDestCheck,
|
||||
SubnetId: d.cfg.CreateVnicDetails.SubnetId,
|
||||
DefinedTags: d.cfg.CreateVnicDetails.DefinedTags,
|
||||
FreeformTags: d.cfg.CreateVnicDetails.FreeformTags,
|
||||
}
|
||||
|
||||
instanceDetails.CreateVnicDetails = &CreateVnicDetails
|
||||
|
||||
// Create Source details which will be used to Launch Instance
|
||||
instanceDetails.SourceDetails = core.InstanceSourceViaImageDetails{
|
||||
ImageId: &d.cfg.BaseImageID,
|
||||
BootVolumeSizeInGBs: &d.cfg.BootVolumeSizeInGBs,
|
||||
}
|
||||
|
||||
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: instanceDetails})
|
||||
|
||||
if err != nil {
|
||||
|
@ -79,7 +99,7 @@ func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (strin
|
|||
// CreateImage creates a new custom image.
|
||||
func (d *driverOCI) CreateImage(ctx context.Context, id string) (core.Image, error) {
|
||||
res, err := d.computeClient.CreateImage(ctx, core.CreateImageRequest{CreateImageDetails: core.CreateImageDetails{
|
||||
CompartmentId: &d.cfg.CompartmentID,
|
||||
CompartmentId: &d.cfg.ImageCompartmentID,
|
||||
InstanceId: &id,
|
||||
DisplayName: &d.cfg.ImageName,
|
||||
FreeformTags: d.cfg.Tags,
|
||||
|
|
|
@ -20,6 +20,7 @@ type FlatConfig struct {
|
|||
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
|
||||
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
|
||||
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
|
||||
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
|
||||
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
|
||||
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
|
||||
|
@ -123,6 +124,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
|
||||
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
|
||||
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
|
||||
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
|
||||
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
|
||||
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
|
||||
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
|
||||
|
|
|
@ -2,6 +2,7 @@ package proxmox
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
|
||||
|
@ -10,23 +11,33 @@ import (
|
|||
)
|
||||
|
||||
type proxmoxDriver struct {
|
||||
client commandTyper
|
||||
vmRef *proxmox.VmRef
|
||||
specialMap map[string]string
|
||||
runeMap map[rune]string
|
||||
interval time.Duration
|
||||
client commandTyper
|
||||
vmRef *proxmox.VmRef
|
||||
specialMap map[string]string
|
||||
runeMap map[rune]string
|
||||
interval time.Duration
|
||||
specialBuffer []string
|
||||
normalBuffer []string
|
||||
}
|
||||
|
||||
func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Duration) *proxmoxDriver {
|
||||
// Mappings for packer shorthand to qemu qkeycodes
|
||||
sMap := map[string]string{
|
||||
"spacebar": "spc",
|
||||
"bs": "backspace",
|
||||
"del": "delete",
|
||||
"return": "ret",
|
||||
"enter": "ret",
|
||||
"pageUp": "pgup",
|
||||
"pageDown": "pgdn",
|
||||
"spacebar": "spc",
|
||||
"bs": "backspace",
|
||||
"del": "delete",
|
||||
"return": "ret",
|
||||
"enter": "ret",
|
||||
"pageUp": "pgup",
|
||||
"pageDown": "pgdn",
|
||||
"leftshift": "shift",
|
||||
"rightshift": "shift",
|
||||
"leftalt": "alt",
|
||||
"rightalt": "alt_r",
|
||||
"leftctrl": "ctrl",
|
||||
"rightctrl": "ctrl_r",
|
||||
"leftsuper": "meta_l",
|
||||
"rightsuper": "meta_r",
|
||||
}
|
||||
// Mappings for runes that need to be translated to special qkeycodes
|
||||
// Taken from https://github.com/qemu/qemu/blob/master/pc-bios/keymaps/en-us
|
||||
|
@ -78,18 +89,26 @@ func NewProxmoxDriver(c commandTyper, vmRef *proxmox.VmRef, interval time.Durati
|
|||
}
|
||||
|
||||
func (p *proxmoxDriver) SendKey(key rune, action bootcommand.KeyAction) error {
|
||||
if special, ok := p.runeMap[key]; ok {
|
||||
return p.send(special)
|
||||
switch action.String() {
|
||||
case "Press":
|
||||
if special, ok := p.runeMap[key]; ok {
|
||||
return p.send(special)
|
||||
}
|
||||
var keys string
|
||||
if unicode.IsUpper(key) {
|
||||
keys = fmt.Sprintf("shift-%c", unicode.ToLower(key))
|
||||
} else {
|
||||
keys = fmt.Sprintf("%c", key)
|
||||
}
|
||||
return p.send(keys)
|
||||
case "On":
|
||||
key := fmt.Sprintf("%c", key)
|
||||
p.normalBuffer = addKeyToBuffer(p.normalBuffer, key)
|
||||
case "Off":
|
||||
key := fmt.Sprintf("%c", key)
|
||||
p.normalBuffer = removeKeyFromBuffer(p.normalBuffer, key)
|
||||
}
|
||||
|
||||
var keys string
|
||||
if unicode.IsUpper(key) {
|
||||
keys = fmt.Sprintf("shift-%c", unicode.ToLower(key))
|
||||
} else {
|
||||
keys = fmt.Sprintf("%c", key)
|
||||
}
|
||||
|
||||
return p.send(keys)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction) error {
|
||||
|
@ -97,18 +116,48 @@ func (p *proxmoxDriver) SendSpecial(special string, action bootcommand.KeyAction
|
|||
if replacement, ok := p.specialMap[special]; ok {
|
||||
keys = replacement
|
||||
}
|
||||
|
||||
return p.send(keys)
|
||||
switch action.String() {
|
||||
case "Press":
|
||||
return p.send(keys)
|
||||
case "On":
|
||||
p.specialBuffer = addKeyToBuffer(p.specialBuffer, keys)
|
||||
case "Off":
|
||||
p.specialBuffer = removeKeyFromBuffer(p.specialBuffer, keys)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) send(keys string) error {
|
||||
err := p.client.Sendkey(p.vmRef, keys)
|
||||
func (p *proxmoxDriver) send(key string) error {
|
||||
keys := append(p.specialBuffer, p.normalBuffer...)
|
||||
keys = append(keys, key)
|
||||
keyEventString := bufferToKeyEvent(keys)
|
||||
err := p.client.Sendkey(p.vmRef, keyEventString)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
time.Sleep(p.interval)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *proxmoxDriver) Flush() error { return nil }
|
||||
|
||||
func bufferToKeyEvent(keys []string) string {
|
||||
return strings.Join(keys, "-")
|
||||
}
|
||||
func addKeyToBuffer(buffer []string, key string) []string {
|
||||
for _, value := range buffer {
|
||||
if value == key {
|
||||
return buffer
|
||||
}
|
||||
}
|
||||
return append(buffer, key)
|
||||
}
|
||||
func removeKeyFromBuffer(buffer []string, key string) []string {
|
||||
for index, value := range buffer {
|
||||
if value == key {
|
||||
buffer[index] = buffer[len(buffer)-1]
|
||||
return buffer[:len(buffer)-1]
|
||||
}
|
||||
}
|
||||
return buffer
|
||||
}
|
||||
|
|
|
@ -131,16 +131,32 @@ func commHost(host string) func(state multistep.StateBag) (string, error) {
|
|||
// Reads the first non-loopback interface's IP address from the VM.
|
||||
// qemu-guest-agent package must be installed on the VM
|
||||
func getVMIP(state multistep.StateBag) (string, error) {
|
||||
c := state.Get("proxmoxClient").(*proxmox.Client)
|
||||
client := state.Get("proxmoxClient").(*proxmox.Client)
|
||||
config := state.Get("config").(*Config)
|
||||
vmRef := state.Get("vmRef").(*proxmox.VmRef)
|
||||
|
||||
ifs, err := c.GetVmAgentNetworkInterfaces(vmRef)
|
||||
ifs, err := client.GetVmAgentNetworkInterfaces(vmRef)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// TODO: Do something smarter here? Allow specifying interface? Or address family?
|
||||
// For now, just go for first non-loopback
|
||||
if config.VMInterface != "" {
|
||||
for _, iface := range ifs {
|
||||
if config.VMInterface != iface.Name {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, addr := range iface.IPAddresses {
|
||||
if addr.IsLoopback() {
|
||||
continue
|
||||
}
|
||||
return addr.String(), nil
|
||||
}
|
||||
return "", fmt.Errorf("Interface %s only has loopback addresses", config.VMInterface)
|
||||
}
|
||||
return "", fmt.Errorf("Interface %s not found in VM", config.VMInterface)
|
||||
}
|
||||
|
||||
for _, iface := range ifs {
|
||||
for _, addr := range iface.IPAddresses {
|
||||
if addr.IsLoopback() {
|
||||
|
|
|
@ -58,6 +58,11 @@ type Config struct {
|
|||
CloudInit bool `mapstructure:"cloud_init"`
|
||||
CloudInitStoragePool string `mapstructure:"cloud_init_storage_pool"`
|
||||
|
||||
shouldUploadISO bool
|
||||
|
||||
AdditionalISOFiles []storageConfig `mapstructure:"additional_iso_files"`
|
||||
VMInterface string `mapstructure:"vm_interface"`
|
||||
|
||||
Ctx interpolate.Context `mapstructure:",squash",mapstructure-to-hcl2:"skip"`
|
||||
}
|
||||
|
||||
|
@ -76,6 +81,7 @@ 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"`
|
||||
|
@ -172,6 +178,17 @@ func (c *Config) Prepare(upper interface{}, raws ...interface{}) ([]string, []st
|
|||
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!
|
||||
|
|
|
@ -133,6 +133,10 @@ 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
|
||||
}
|
||||
|
|
|
@ -53,14 +53,20 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
httpIP, err := hostIP()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Failed to determine host IP: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
var httpIP string
|
||||
var err error
|
||||
if c.HTTPAddress != "0.0.0.0" {
|
||||
httpIP = c.HTTPAddress
|
||||
} else {
|
||||
httpIP, err = hostIP(c.HTTPInterface)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Failed to determine host IP: %s", err)
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
state.Put("http_ip", httpIP)
|
||||
s.Ctx.Data = &bootCommandTemplateData{
|
||||
HTTPIP: httpIP,
|
||||
|
@ -97,12 +103,25 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
|
|||
|
||||
func (*stepTypeBootCommand) Cleanup(multistep.StateBag) {}
|
||||
|
||||
func hostIP() (string, error) {
|
||||
addrs, err := net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
func hostIP(ifname string) (string, error) {
|
||||
var addrs []net.Addr
|
||||
var err error
|
||||
|
||||
if ifname != "" {
|
||||
iface, err := net.InterfaceByName(ifname)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
addrs, err = iface.Addrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
addrs, err = net.InterfaceAddrs()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
for _, addr := range addrs {
|
||||
if ipnet, ok := addr.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
|
@ -110,6 +129,5 @@ func hostIP() (string, error) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("No host IP found")
|
||||
}
|
||||
|
|
|
@ -52,6 +52,34 @@ func TestTypeBootCommand(t *testing.T) {
|
|||
expectedKeysSent: "shift-hellospcshift-worldspc2dot0fooshift-1barshift-2baz",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "holding and releasing keys",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<leftShiftOn>hello<rightAltOn>world<leftShiftOff><rightAltOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "shift-hshift-eshift-lshift-lshift-oshift-alt_r-wshift-alt_r-oshift-alt_r-rshift-alt_r-lshift-alt_r-d",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "holding multiple alphabetical keys and shift",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn>n<leftShiftOff><cOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "shift-c-n",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "noop keystrokes",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn><cOff><leftAltOn><leftShiftOff><leftAltOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "noop keystrokes mixed",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{"<cOn><leftShiftOn><cOff>h<leftShiftOff>"}}},
|
||||
expectCallSendkey: true,
|
||||
expectedKeysSent: "shift-h",
|
||||
expectedAction: multistep.ActionContinue,
|
||||
},
|
||||
{
|
||||
name: "without boot command sendkey should not be called",
|
||||
builderConfig: &Config{BootConfig: bootcommand.BootConfig{BootCommand: []string{}}},
|
||||
|
|
|
@ -110,3 +110,112 @@ func TestBasicExampleFromDocsIsValid(t *testing.T) {
|
|||
t.Errorf("Expected CloudInit to be false, got %t", b.config.CloudInit)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentSetToFalse(t *testing.T) {
|
||||
cfg := mandatoryConfig(t)
|
||||
cfg["qemu_agent"] = false
|
||||
|
||||
var c Config
|
||||
warn, err := c.Prepare(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err, warn)
|
||||
}
|
||||
|
||||
if c.Agent != false {
|
||||
t.Errorf("Expected Agent to be false, got %t", c.Agent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPacketQueueSupportForNetworkAdapters(t *testing.T) {
|
||||
drivertests := []struct {
|
||||
expectedToFail bool
|
||||
model string
|
||||
}{
|
||||
{expectedToFail: false, model: "virtio"},
|
||||
{expectedToFail: true, model: "e1000"},
|
||||
{expectedToFail: true, model: "e1000-82540em"},
|
||||
{expectedToFail: true, model: "e1000-82544gc"},
|
||||
{expectedToFail: true, model: "e1000-82545em"},
|
||||
{expectedToFail: true, model: "i82551"},
|
||||
{expectedToFail: true, model: "i82557b"},
|
||||
{expectedToFail: true, model: "i82559er"},
|
||||
{expectedToFail: true, model: "ne2k_isa"},
|
||||
{expectedToFail: true, model: "ne2k_pci"},
|
||||
{expectedToFail: true, model: "pcnet"},
|
||||
{expectedToFail: true, model: "rtl8139"},
|
||||
{expectedToFail: true, model: "vmxnet3"},
|
||||
}
|
||||
|
||||
for _, tt := range drivertests {
|
||||
device := make(map[string]interface{})
|
||||
device["bridge"] = "vmbr0"
|
||||
device["model"] = tt.model
|
||||
device["packet_queues"] = 2
|
||||
|
||||
devices := make([]map[string]interface{}, 0)
|
||||
devices = append(devices, device)
|
||||
|
||||
cfg := mandatoryConfig(t)
|
||||
cfg["network_adapters"] = devices
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
//go:generate struct-markdown
|
||||
//go:generate mapstructure-to-hcl2 -type Config
|
||||
|
||||
package qemu
|
||||
|
||||
import (
|
||||
|
@ -11,561 +8,26 @@ 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"`
|
||||
// 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) {
|
||||
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 _, 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 {
|
||||
warnings, errs := b.config.Prepare(raws...)
|
||||
if errs != nil {
|
||||
return nil, warnings, errs
|
||||
}
|
||||
|
||||
|
@ -579,15 +41,6 @@ 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{
|
||||
|
@ -597,14 +50,12 @@ 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),
|
||||
|
@ -613,9 +64,37 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
Directories: b.config.FloppyConfig.FloppyDirectories,
|
||||
Label: b.config.FloppyConfig.FloppyLabel,
|
||||
},
|
||||
new(stepCreateDisk),
|
||||
new(stepCopyDisk),
|
||||
new(stepResizeDisk),
|
||||
&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(stepHTTPIPDiscover),
|
||||
&common.StepHTTPServer{
|
||||
HTTPDir: b.config.HTTPDir,
|
||||
|
@ -623,58 +102,43 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
HTTPPortMax: b.config.HTTPPortMax,
|
||||
HTTPAddress: b.config.HTTPAddress,
|
||||
},
|
||||
)
|
||||
|
||||
if b.config.CommConfig.Comm.Type != "none" && b.config.NetBridge == "" {
|
||||
steps = append(steps,
|
||||
new(stepPortForward),
|
||||
)
|
||||
}
|
||||
|
||||
steps = append(steps,
|
||||
&stepPortForward{
|
||||
CommunicatorType: b.config.CommConfig.Comm.Type,
|
||||
NetBridge: b.config.NetBridge,
|
||||
},
|
||||
new(stepConfigureVNC),
|
||||
steprun,
|
||||
&stepRun{
|
||||
DiskImage: b.config.DiskImage,
|
||||
},
|
||||
&stepConfigureQMP{
|
||||
QMPSocketPath: b.config.QMPSocketPath,
|
||||
},
|
||||
&stepTypeBootCommand{},
|
||||
)
|
||||
|
||||
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,
|
||||
&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,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
)
|
||||
|
||||
steps = append(steps,
|
||||
&common.StepCleanupTempKeys{
|
||||
Comm: &b.config.CommConfig.Comm,
|
||||
},
|
||||
)
|
||||
steps = append(steps,
|
||||
new(stepShutdown),
|
||||
)
|
||||
|
||||
steps = append(steps,
|
||||
new(stepConvertDisk),
|
||||
&stepConvertDisk{
|
||||
DiskCompression: b.config.DiskCompression,
|
||||
Format: b.config.Format,
|
||||
OutputDir: b.config.OutputDir,
|
||||
SkipCompaction: b.config.SkipCompaction,
|
||||
VMName: b.config.VMName,
|
||||
QemuImgArgs: b.config.QemuImgArgs,
|
||||
},
|
||||
)
|
||||
|
||||
// Setup the state bag
|
||||
|
|
|
@ -1,56 +1,11 @@
|
|||
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{}
|
||||
|
@ -58,601 +13,3 @@ 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_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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,619 @@
|
|||
//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
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
// Code generated by "mapstructure-to-hcl2 -type Config"; DO NOT EDIT.
|
||||
// Code generated by "mapstructure-to-hcl2 -type Config,QemuImgArgs"; DO NOT EDIT.
|
||||
package qemu
|
||||
|
||||
import (
|
||||
|
@ -20,6 +20,7 @@ type FlatConfig struct {
|
|||
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
|
||||
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
|
||||
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
|
||||
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
|
||||
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
|
||||
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
|
||||
|
@ -87,12 +88,15 @@ 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"`
|
||||
CpuCount *int `mapstructure:"cpus" required:"false" cty:"cpus" hcl:"cpus"`
|
||||
DiskInterface *string `mapstructure:"disk_interface" required:"false" cty:"disk_interface" hcl:"disk_interface"`
|
||||
DiskSize *string `mapstructure:"disk_size" required:"false" cty:"disk_size" hcl:"disk_size"`
|
||||
SkipResizeDisk *bool `mapstructure:"skip_resize_disk" required:"false" cty:"skip_resize_disk" hcl:"skip_resize_disk"`
|
||||
DiskCache *string `mapstructure:"disk_cache" required:"false" cty:"disk_cache" hcl:"disk_cache"`
|
||||
DiskDiscard *string `mapstructure:"disk_discard" required:"false" cty:"disk_discard" hcl:"disk_discard"`
|
||||
DetectZeroes *string `mapstructure:"disk_detect_zeroes" required:"false" cty:"disk_detect_zeroes" hcl:"disk_detect_zeroes"`
|
||||
|
@ -108,6 +112,7 @@ 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"`
|
||||
|
@ -145,6 +150,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
|
||||
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
|
||||
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
|
||||
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
|
||||
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
|
||||
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
|
||||
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
|
||||
|
@ -212,12 +218,15 @@ 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},
|
||||
"cpus": &hcldec.AttrSpec{Name: "cpus", Type: cty.Number, Required: false},
|
||||
"disk_interface": &hcldec.AttrSpec{Name: "disk_interface", Type: cty.String, Required: false},
|
||||
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.String, Required: false},
|
||||
"skip_resize_disk": &hcldec.AttrSpec{Name: "skip_resize_disk", Type: cty.Bool, Required: false},
|
||||
"disk_cache": &hcldec.AttrSpec{Name: "disk_cache", Type: cty.String, Required: false},
|
||||
"disk_discard": &hcldec.AttrSpec{Name: "disk_discard", Type: cty.String, Required: false},
|
||||
"disk_detect_zeroes": &hcldec.AttrSpec{Name: "disk_detect_zeroes", Type: cty.String, Required: false},
|
||||
|
@ -233,6 +242,7 @@ 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},
|
||||
|
@ -248,3 +258,30 @@ 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
|
||||
}
|
|
@ -0,0 +1,694 @@
|
|||
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")
|
||||
}
|
|
@ -6,6 +6,7 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
@ -22,6 +23,10 @@ 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
|
||||
|
||||
|
@ -65,6 +70,33 @@ 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()
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
package qemu
|
||||
|
||||
import "sync"
|
||||
|
||||
type DriverMock struct {
|
||||
sync.Mutex
|
||||
|
||||
CopyCalled bool
|
||||
CopyErr error
|
||||
|
||||
StopCalled bool
|
||||
StopErr error
|
||||
|
||||
QemuCalls [][]string
|
||||
QemuErrs []error
|
||||
|
||||
WaitForShutdownCalled bool
|
||||
WaitForShutdownState bool
|
||||
|
||||
QemuImgCalled bool
|
||||
QemuImgCalls []string
|
||||
QemuImgErrs []error
|
||||
|
||||
VerifyCalled bool
|
||||
VerifyErr error
|
||||
|
||||
VersionCalled bool
|
||||
VersionResult string
|
||||
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
|
||||
}
|
||||
|
||||
func (d *DriverMock) Qemu(args ...string) error {
|
||||
d.QemuCalls = append(d.QemuCalls, args)
|
||||
|
||||
if len(d.QemuErrs) >= len(d.QemuCalls) {
|
||||
return d.QemuErrs[len(d.QemuCalls)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DriverMock) WaitForShutdown(cancelCh <-chan struct{}) bool {
|
||||
d.WaitForShutdownCalled = true
|
||||
return d.WaitForShutdownState
|
||||
}
|
||||
|
||||
func (d *DriverMock) QemuImg(args ...string) error {
|
||||
d.QemuImgCalled = true
|
||||
d.QemuImgCalls = append(d.QemuImgCalls, args...)
|
||||
|
||||
if len(d.QemuImgErrs) >= len(d.QemuImgCalls) {
|
||||
return d.QemuImgErrs[len(d.QemuImgCalls)-1]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *DriverMock) Verify() error {
|
||||
d.VerifyCalled = true
|
||||
return d.VerifyErr
|
||||
}
|
||||
|
||||
func (d *DriverMock) Version() (string, error) {
|
||||
d.VersionCalled = true
|
||||
return d.VersionResult, d.VersionErr
|
||||
}
|
|
@ -17,37 +17,32 @@ import (
|
|||
|
||||
// This step converts the virtual disk that was used as the
|
||||
// hard drive for the virtual machine.
|
||||
type stepConvertDisk struct{}
|
||||
type stepConvertDisk struct {
|
||||
DiskCompression bool
|
||||
Format string
|
||||
OutputDir string
|
||||
SkipCompaction bool
|
||||
VMName string
|
||||
|
||||
QemuImgArgs QemuImgArgs
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
if config.SkipCompaction && !config.DiskCompression {
|
||||
diskName := s.VMName
|
||||
|
||||
if s.SkipCompaction && !s.DiskCompression {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
name := diskName + ".convert"
|
||||
|
||||
sourcePath := filepath.Join(config.OutputDir, diskName)
|
||||
targetPath := filepath.Join(config.OutputDir, name)
|
||||
sourcePath := filepath.Join(s.OutputDir, diskName)
|
||||
targetPath := filepath.Join(s.OutputDir, name)
|
||||
|
||||
command := []string{
|
||||
"convert",
|
||||
}
|
||||
|
||||
if config.DiskCompression {
|
||||
command = append(command, "-c")
|
||||
}
|
||||
|
||||
command = append(command, []string{
|
||||
"-O", config.Format,
|
||||
sourcePath,
|
||||
targetPath,
|
||||
}...,
|
||||
)
|
||||
command := s.buildConvertCommand(sourcePath, targetPath)
|
||||
|
||||
ui.Say("Converting hard drive...")
|
||||
// Retry the conversion a few times in case it takes the qemu process a
|
||||
|
@ -90,4 +85,20 @@ 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) {}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
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))
|
||||
}
|
||||
}
|
|
@ -11,26 +11,42 @@ import (
|
|||
|
||||
// This step copies the virtual disk that will be used as the
|
||||
// hard drive for the virtual machine.
|
||||
type stepCopyDisk struct{}
|
||||
type stepCopyDisk struct {
|
||||
DiskImage bool
|
||||
Format string
|
||||
OutputDir string
|
||||
UseBackingFile bool
|
||||
VMName string
|
||||
|
||||
QemuImgArgs QemuImgArgs
|
||||
}
|
||||
|
||||
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(config.OutputDir, config.VMName)
|
||||
path := filepath.Join(s.OutputDir, s.VMName)
|
||||
|
||||
command := []string{
|
||||
"convert",
|
||||
"-O", config.Format,
|
||||
isoPath,
|
||||
path,
|
||||
}
|
||||
|
||||
if !config.DiskImage || config.UseBackingFile {
|
||||
if !s.DiskImage || s.UseBackingFile {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
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)
|
||||
|
@ -42,4 +58,16 @@ 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) {}
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
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")
|
||||
}
|
|
@ -12,15 +12,23 @@ import (
|
|||
|
||||
// This step creates the virtual disk that will be used as the
|
||||
// hard drive for the virtual machine.
|
||||
type stepCreateDisk struct{}
|
||||
type stepCreateDisk struct {
|
||||
AdditionalDiskSize []string
|
||||
DiskImage bool
|
||||
DiskSize string
|
||||
Format string
|
||||
OutputDir string
|
||||
UseBackingFile bool
|
||||
VMName string
|
||||
QemuImgArgs QemuImgArgs
|
||||
}
|
||||
|
||||
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 := config.VMName
|
||||
name := s.VMName
|
||||
|
||||
if config.DiskImage && !config.UseBackingFile {
|
||||
if s.DiskImage && !s.UseBackingFile {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
@ -28,12 +36,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(config.OutputDir, name))
|
||||
diskSizes = append(diskSizes, config.DiskSize)
|
||||
diskFullPaths = append(diskFullPaths, filepath.Join(s.OutputDir, name))
|
||||
diskSizes = append(diskSizes, s.DiskSize)
|
||||
// Additional disks
|
||||
if len(config.AdditionalDiskSize) > 0 {
|
||||
for i, diskSize := range config.AdditionalDiskSize {
|
||||
path := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d", name, i+1))
|
||||
if len(s.AdditionalDiskSize) > 0 {
|
||||
for i, diskSize := range s.AdditionalDiskSize {
|
||||
path := filepath.Join(s.OutputDir, fmt.Sprintf("%s-%d", name, i+1))
|
||||
diskFullPaths = append(diskFullPaths, path)
|
||||
size := diskSize
|
||||
diskSizes = append(diskSizes, size)
|
||||
|
@ -43,19 +51,8 @@ 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,
|
||||
}
|
||||
|
||||
if config.UseBackingFile && i == 0 {
|
||||
isoPath := state.Get("iso_path").(string)
|
||||
command = append(command, "-b", isoPath)
|
||||
}
|
||||
|
||||
command = append(command,
|
||||
diskFullPath,
|
||||
diskSizes[i])
|
||||
command := s.buildCreateCommand(diskFullPath, diskSizes[i], i, state)
|
||||
|
||||
if err := driver.QemuImg(command...); err != nil {
|
||||
err := fmt.Errorf("Error creating hard drive: %s", err)
|
||||
|
@ -71,4 +68,21 @@ 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) {}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
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))
|
||||
}
|
||||
}
|
|
@ -13,6 +13,9 @@ 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
|
||||
}
|
||||
|
||||
|
@ -20,6 +23,15 @@ 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 {
|
||||
|
|
|
@ -11,21 +11,26 @@ import (
|
|||
|
||||
// This step resizes the virtual disk that will be used as the
|
||||
// hard drive for the virtual machine.
|
||||
type stepResizeDisk struct{}
|
||||
type stepResizeDisk struct {
|
||||
DiskCompression bool
|
||||
DiskImage bool
|
||||
Format string
|
||||
OutputDir string
|
||||
SkipResizeDisk bool
|
||||
VMName string
|
||||
DiskSize string
|
||||
|
||||
QemuImgArgs QemuImgArgs
|
||||
}
|
||||
|
||||
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(config.OutputDir, config.VMName)
|
||||
path := filepath.Join(s.OutputDir, s.VMName)
|
||||
|
||||
command := []string{
|
||||
"resize",
|
||||
"-f", config.Format,
|
||||
path,
|
||||
config.DiskSize,
|
||||
}
|
||||
if config.DiskImage == false {
|
||||
command := s.buildResizeCommand(path)
|
||||
|
||||
if s.DiskImage == false || s.SkipResizeDisk == true {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
@ -40,4 +45,16 @@ 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) {}
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
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,
|
||||
},
|
||||
}
|
||||
for _, config := range testConfigs {
|
||||
state := testState(t)
|
||||
driver := state.Get("driver").(*DriverMock)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func Test_buildResizeCommand(t *testing.T) {
|
||||
type testCase struct {
|
||||
Step *stepResizeDisk
|
||||
Expected []string
|
||||
Reason string
|
||||
}
|
||||
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",
|
||||
},
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
|
@ -15,8 +15,7 @@ import (
|
|||
|
||||
// stepRun runs the virtual machine
|
||||
type stepRun struct {
|
||||
BootDrive string
|
||||
Message string
|
||||
DiskImage bool
|
||||
}
|
||||
|
||||
type qemuArgsTemplateData struct {
|
||||
|
@ -32,9 +31,17 @@ func (s *stepRun) Run(ctx context.Context, state multistep.StateBag) multistep.S
|
|||
driver := state.Get("driver").(Driver)
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say(s.Message)
|
||||
// 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)
|
||||
|
||||
command, err := getCommandArgs(s.BootDrive, state)
|
||||
command, err := getCommandArgs(bootDrive, state)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing QemuArgs: %s", err)
|
||||
ui.Error(err.Error())
|
||||
|
@ -73,12 +80,11 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
var deviceArgs []string
|
||||
var driveArgs []string
|
||||
var commHostPort int
|
||||
var vnc string
|
||||
|
||||
if !config.VNCUsePassword {
|
||||
vnc = fmt.Sprintf("%s:%d", vncIP, vncPort-5900)
|
||||
} else {
|
||||
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort-5900)
|
||||
vncPort = vncPort - config.VNCPortMin
|
||||
vnc := fmt.Sprintf("%s:%d", vncIP, vncPort)
|
||||
if config.VNCUsePassword {
|
||||
vnc = fmt.Sprintf("%s:%d,password", vncIP, vncPort)
|
||||
}
|
||||
|
||||
if config.QMPEnable {
|
||||
|
@ -191,14 +197,26 @@ 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 == "" {
|
||||
defaultArgs["-cdrom"] = isoPath
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,index=%d,media=cdrom", cdPath, i))
|
||||
} else if config.CDROMInterface == "virtio-scsi" {
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=none,id=cdrom,media=cdrom", isoPath))
|
||||
deviceArgs = append(deviceArgs, "virtio-scsi-device", "scsi-cd,drive=cdrom")
|
||||
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))
|
||||
} else {
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,id=cdrom,media=cdrom", isoPath, config.CDROMInterface))
|
||||
driveArgs = append(driveArgs, fmt.Sprintf("file=%s,if=%s,index=%d,id=cdrom%d,media=cdrom", cdPath, config.CDROMInterface, i, i))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,7 +247,7 @@ func getCommandArgs(bootDrive string, state multistep.StateBag) ([]string, error
|
|||
|
||||
inArgs := make(map[string][]string)
|
||||
if len(config.QemuArgs) > 0 {
|
||||
ui.Say("Overriding defaults Qemu arguments with QemuArgs...")
|
||||
ui.Say("Overriding default Qemu arguments with QemuArgs...")
|
||||
|
||||
httpIp := state.Get("http_ip").(string)
|
||||
httpPort := state.Get("http_port").(int)
|
||||
|
|
|
@ -0,0 +1,148 @@
|
|||
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.")
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
package qemu
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
)
|
||||
|
||||
func testState(t *testing.T) multistep.StateBag {
|
||||
state := new(multistep.BasicStateBag)
|
||||
state.Put("driver", new(DriverMock))
|
||||
state.Put("ui", &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
})
|
||||
return state
|
||||
}
|
|
@ -19,19 +19,31 @@ 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...", config.NetBridge))
|
||||
ui.Say(fmt.Sprintf("Waiting for the guest address to become available in the %s network bridge...", s.NetBridge))
|
||||
for {
|
||||
guestAddress := getGuestAddress(qmpMonitor, config.NetBridge, "user.0")
|
||||
guestAddress := getGuestAddress(qmpMonitor, s.NetBridge, "user.0")
|
||||
if guestAddress != "" {
|
||||
log.Printf("Found guest address %s", guestAddress)
|
||||
state.Put("guestAddress", guestAddress)
|
||||
|
|
|
@ -4,7 +4,8 @@ import (
|
|||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
|
@ -20,11 +21,11 @@ type Artifact struct {
|
|||
// The ID of the snapshot
|
||||
snapshotID string
|
||||
|
||||
// The name of the region
|
||||
regionName string
|
||||
// The name of the zone
|
||||
zoneName string
|
||||
|
||||
// The client for making API calls
|
||||
client *api.ScalewayAPI
|
||||
client *scw.Client
|
||||
|
||||
// StateData should store data such as GeneratedData
|
||||
// to be shared with post-processors
|
||||
|
@ -41,12 +42,12 @@ func (*Artifact) Files() []string {
|
|||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return fmt.Sprintf("%s:%s", a.regionName, a.imageID)
|
||||
return fmt.Sprintf("%s:%s", a.zoneName, a.imageID)
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("An image was created: '%v' (ID: %v) in region '%v' based on snapshot '%v' (ID: %v)",
|
||||
a.imageName, a.imageID, a.regionName, a.snapshotName, a.snapshotID)
|
||||
return fmt.Sprintf("An image was created: '%v' (ID: %v) in zone '%v' based on snapshot '%v' (ID: %v)",
|
||||
a.imageName, a.imageID, a.zoneName, a.snapshotName, a.snapshotID)
|
||||
}
|
||||
|
||||
func (a *Artifact) State(name string) interface{} {
|
||||
|
@ -55,11 +56,19 @@ func (a *Artifact) State(name string) interface{} {
|
|||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %s (%s)", a.imageID, a.imageName)
|
||||
if err := a.client.DeleteImage(a.imageID); err != nil {
|
||||
instanceAPI := instance.NewAPI(a.client)
|
||||
|
||||
err := instanceAPI.DeleteImage(&instance.DeleteImageRequest{
|
||||
ImageID: a.imageID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("Destroying snapshot: %s (%s)", a.snapshotID, a.snapshotName)
|
||||
if err := a.client.DeleteSnapshot(a.snapshotID); err != nil {
|
||||
err = instanceAPI.DeleteSnapshot(&instance.DeleteSnapshotRequest{
|
||||
SnapshotID: a.snapshotID,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
|
|
@ -27,7 +27,7 @@ func TestArtifactId(t *testing.T) {
|
|||
func TestArtifactString(t *testing.T) {
|
||||
generatedData := make(map[string]interface{})
|
||||
a := &Artifact{"packer-foobar-image", "cc586e45-5156-4f71-b223-cf406b10dd1d", "packer-foobar-snapshot", "cc586e45-5156-4f71-b223-cf406b10dd1c", "ams1", nil, generatedData}
|
||||
expected := "An image was created: 'packer-foobar-image' (ID: cc586e45-5156-4f71-b223-cf406b10dd1d) in region 'ams1' based on snapshot 'packer-foobar-snapshot' (ID: cc586e45-5156-4f71-b223-cf406b10dd1c)"
|
||||
expected := "An image was created: 'packer-foobar-image' (ID: cc586e45-5156-4f71-b223-cf406b10dd1d) in zone 'ams1' based on snapshot 'packer-foobar-snapshot' (ID: cc586e45-5156-4f71-b223-cf406b10dd1c)"
|
||||
|
||||
if a.String() != expected {
|
||||
t.Fatalf("artifact string should match: %v", expected)
|
||||
|
|
|
@ -13,7 +13,7 @@ import (
|
|||
"github.com/hashicorp/packer/helper/communicator"
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
// The unique id for the builder
|
||||
|
@ -32,12 +32,27 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
|
|||
return nil, warnings, errs
|
||||
}
|
||||
|
||||
return nil, nil, nil
|
||||
return nil, warnings, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (packer.Artifact, error) {
|
||||
client, err := api.NewScalewayAPI(b.config.Organization, b.config.Token, b.config.UserAgent, b.config.Region)
|
||||
scwZone, err := scw.ParseZone(b.config.Zone)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientOpts := []scw.ClientOption{
|
||||
scw.WithDefaultProjectID(b.config.ProjectID),
|
||||
scw.WithAuth(b.config.AccessKey, b.config.SecretKey),
|
||||
scw.WithDefaultZone(scwZone),
|
||||
}
|
||||
|
||||
if b.config.APIURL != "" {
|
||||
clientOpts = append(clientOpts, scw.WithAPIURL(b.config.APIURL))
|
||||
}
|
||||
|
||||
client, err := scw.NewClient(clientOpts...)
|
||||
if err != nil {
|
||||
ui.Error(err.Error())
|
||||
return nil, err
|
||||
|
@ -50,6 +65,11 @@ 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),
|
||||
|
@ -96,7 +116,7 @@ func (b *Builder) Run(ctx context.Context, ui packer.Ui, hook packer.Hook) (pack
|
|||
imageID: state.Get("image_id").(string),
|
||||
snapshotName: state.Get("snapshot_name").(string),
|
||||
snapshotID: state.Get("snapshot_id").(string),
|
||||
regionName: state.Get("region").(string),
|
||||
zoneName: b.config.Zone,
|
||||
client: client,
|
||||
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||
}
|
||||
|
|
|
@ -9,9 +9,10 @@ import (
|
|||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"organization_id": "foo",
|
||||
"api_token": "bar",
|
||||
"region": "ams1",
|
||||
"project_id": "00000000-1111-2222-3333-444444444444",
|
||||
"access_key": "SCWABCXXXXXXXXXXXXXX",
|
||||
"secret_key": "00000000-1111-2222-3333-444444444444",
|
||||
"zone": "fr-par-1",
|
||||
"commercial_type": "START1-S",
|
||||
"ssh_username": "root",
|
||||
"image": "image-uuid",
|
||||
|
@ -32,10 +33,7 @@ func TestBuilder_Prepare_BadType(t *testing.T) {
|
|||
"api_token": []string{},
|
||||
}
|
||||
|
||||
_, warnings, err := b.Prepare(c)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
}
|
||||
_, _, err := b.Prepare(c)
|
||||
if err == nil {
|
||||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
|
@ -68,11 +66,11 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_Region(t *testing.T) {
|
||||
func TestBuilderPrepare_Zone(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "region")
|
||||
delete(config, "zone")
|
||||
_, warnings, err := b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
t.Fatalf("bad: %#v", warnings)
|
||||
|
@ -81,9 +79,9 @@ func TestBuilderPrepare_Region(t *testing.T) {
|
|||
t.Fatalf("should error")
|
||||
}
|
||||
|
||||
expected := "ams1"
|
||||
expected := "fr-par-1"
|
||||
|
||||
config["region"] = expected
|
||||
config["zone"] = expected
|
||||
b = Builder{}
|
||||
_, warnings, err = b.Prepare(config)
|
||||
if len(warnings) > 0 {
|
||||
|
@ -93,8 +91,8 @@ func TestBuilderPrepare_Region(t *testing.T) {
|
|||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.Region != expected {
|
||||
t.Errorf("found %s, expected %s", b.config.Region, expected)
|
||||
if b.config.Zone != expected {
|
||||
t.Errorf("found %s, expected %s", b.config.Zone, expected)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,27 +17,29 @@ import (
|
|||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
Comm communicator.Config `mapstructure:",squash"`
|
||||
// The token to use to authenticate with your account.
|
||||
// It can also be specified via environment variable SCALEWAY_API_TOKEN. You
|
||||
// can see and generate tokens in the "Credentials"
|
||||
// section of the control panel.
|
||||
Token string `mapstructure:"api_token" required:"true"`
|
||||
// The organization id to use to identify your
|
||||
// organization. It can also be specified via environment variable
|
||||
// SCALEWAY_ORGANIZATION. Your organization id is available in the
|
||||
// "Account" section of the
|
||||
// control panel.
|
||||
// Previously named: api_access_key with environment variable: SCALEWAY_API_ACCESS_KEY
|
||||
Organization string `mapstructure:"organization_id" required:"true"`
|
||||
// The name of the region to launch the server in (par1
|
||||
// or ams1). Consequently, this is the region where the snapshot will be
|
||||
// available.
|
||||
Region string `mapstructure:"region" required:"true"`
|
||||
// The AccessKey corresponding to the secret key.
|
||||
// It can also be specified via the environment variable SCW_ACCESS_KEY.
|
||||
AccessKey string `mapstructure:"access_key" required:"true"`
|
||||
// The SecretKey to authenticate against the Scaleway API.
|
||||
// It can also be specified via the environment variable SCW_SECRET_KEY.
|
||||
SecretKey string `mapstructure:"secret_key" required:"true"`
|
||||
// The Project ID in which the instances, volumes and snapshots will be created.
|
||||
// It can also be specified via the environment variable SCW_DEFAULT_PROJECT_ID.
|
||||
ProjectID string `mapstructure:"project_id" required:"true"`
|
||||
// The Zone in which the instances, volumes and snapshots will be created.
|
||||
// It can also be specified via the environment variable SCW_DEFAULT_ZONE
|
||||
Zone string `mapstructure:"zone" required:"true"`
|
||||
// The Scaleway API URL to use
|
||||
// It can also be specified via the environment variable SCW_API_URL
|
||||
APIURL string `mapstructure:"api_url"`
|
||||
|
||||
// The UUID of the base image to use. This is the image
|
||||
// that will be used to launch a new server and provision it. See
|
||||
// the images list
|
||||
|
@ -69,6 +71,28 @@ type Config struct {
|
|||
|
||||
UserAgent string `mapstructure-to-hcl2:",skip"`
|
||||
ctx interpolate.Context
|
||||
|
||||
// Deprecated configs
|
||||
|
||||
// The token to use to authenticate with your account.
|
||||
// It can also be specified via environment variable SCALEWAY_API_TOKEN. You
|
||||
// can see and generate tokens in the "Credentials"
|
||||
// section of the control panel.
|
||||
// Deprecated, use SecretKey instead
|
||||
Token string `mapstructure:"api_token" required:"false"`
|
||||
// The organization id to use to identify your
|
||||
// organization. It can also be specified via environment variable
|
||||
// SCALEWAY_ORGANIZATION. Your organization id is available in the
|
||||
// "Account" section of the
|
||||
// control panel.
|
||||
// Previously named: api_access_key with environment variable: SCALEWAY_API_ACCESS_KEY
|
||||
// Deprecated, use ProjectID instead
|
||||
Organization string `mapstructure:"organization_id" required:"false"`
|
||||
// The name of the region to launch the server in (par1
|
||||
// or ams1). Consequently, this is the region where the snapshot will be
|
||||
// available.
|
||||
// Deprecated, use Zone instead
|
||||
Region string `mapstructure:"region" required:"false"`
|
||||
}
|
||||
|
||||
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
||||
|
@ -88,8 +112,11 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
|||
return nil, err
|
||||
}
|
||||
|
||||
var warnings []string
|
||||
|
||||
c.UserAgent = useragent.String()
|
||||
|
||||
// Deprecated variables
|
||||
if c.Organization == "" {
|
||||
if os.Getenv("SCALEWAY_ORGANIZATION") != "" {
|
||||
c.Organization = os.Getenv("SCALEWAY_ORGANIZATION")
|
||||
|
@ -98,10 +125,43 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
|||
c.Organization = os.Getenv("SCALEWAY_API_ACCESS_KEY")
|
||||
}
|
||||
}
|
||||
if c.Organization != "" {
|
||||
warnings = append(warnings, "organization_id is deprecated in favor of project_id")
|
||||
c.ProjectID = c.Organization
|
||||
}
|
||||
|
||||
if c.Token == "" {
|
||||
c.Token = os.Getenv("SCALEWAY_API_TOKEN")
|
||||
}
|
||||
if c.Token != "" {
|
||||
warnings = append(warnings, "token is deprecated in favor of secret_key")
|
||||
c.SecretKey = c.Token
|
||||
}
|
||||
|
||||
if c.Region != "" {
|
||||
warnings = append(warnings, "region is deprecated in favor of zone")
|
||||
c.Zone = c.Region
|
||||
}
|
||||
|
||||
if c.AccessKey == "" {
|
||||
c.AccessKey = os.Getenv(scw.ScwAccessKeyEnv)
|
||||
}
|
||||
|
||||
if c.SecretKey == "" {
|
||||
c.SecretKey = os.Getenv(scw.ScwSecretKeyEnv)
|
||||
}
|
||||
|
||||
if c.ProjectID == "" {
|
||||
c.ProjectID = os.Getenv(scw.ScwDefaultProjectIDEnv)
|
||||
}
|
||||
|
||||
if c.Zone == "" {
|
||||
c.Zone = os.Getenv(scw.ScwDefaultZoneEnv)
|
||||
}
|
||||
|
||||
if c.APIURL == "" {
|
||||
c.APIURL = os.Getenv(scw.ScwAPIURLEnv)
|
||||
}
|
||||
|
||||
if c.SnapshotName == "" {
|
||||
def, err := interpolate.Render("snapshot-packer-{{timestamp}}", nil)
|
||||
|
@ -127,26 +187,31 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
if c.BootType == "" {
|
||||
c.BootType = "local"
|
||||
c.BootType = instance.BootTypeLocal.String()
|
||||
}
|
||||
|
||||
var errs *packer.MultiError
|
||||
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||
errs = packer.MultiErrorAppend(errs, es...)
|
||||
}
|
||||
if c.Organization == "" {
|
||||
if c.ProjectID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Scaleway Organization ID must be specified"))
|
||||
errs, errors.New("Scaleway Project ID must be specified"))
|
||||
}
|
||||
|
||||
if c.Token == "" {
|
||||
if c.SecretKey == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Scaleway Token must be specified"))
|
||||
errs, errors.New("Scaleway Secret Key must be specified"))
|
||||
}
|
||||
|
||||
if c.Region == "" {
|
||||
if c.AccessKey == "" {
|
||||
warnings = append(warnings, "access_key will be required in future versions")
|
||||
c.AccessKey = "SCWXXXXXXXXXXXXXXXXX"
|
||||
}
|
||||
|
||||
if c.Zone == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("region is required"))
|
||||
errs, errors.New("Scaleway Zone is required"))
|
||||
}
|
||||
|
||||
if c.CommercialType == "" {
|
||||
|
@ -160,9 +225,9 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
|
|||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return nil, errs
|
||||
return warnings, errs
|
||||
}
|
||||
|
||||
packer.LogSecretFilter.Set(c.Token)
|
||||
return nil, nil
|
||||
return warnings, nil
|
||||
}
|
||||
|
|
|
@ -63,9 +63,11 @@ type FlatConfig struct {
|
|||
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
|
||||
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"`
|
||||
Token *string `mapstructure:"api_token" required:"true" cty:"api_token" hcl:"api_token"`
|
||||
Organization *string `mapstructure:"organization_id" required:"true" cty:"organization_id" hcl:"organization_id"`
|
||||
Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"`
|
||||
AccessKey *string `mapstructure:"access_key" required:"true" cty:"access_key" hcl:"access_key"`
|
||||
SecretKey *string `mapstructure:"secret_key" required:"true" cty:"secret_key" hcl:"secret_key"`
|
||||
ProjectID *string `mapstructure:"project_id" required:"true" cty:"project_id" hcl:"project_id"`
|
||||
Zone *string `mapstructure:"zone" required:"true" cty:"zone" hcl:"zone"`
|
||||
APIURL *string `mapstructure:"api_url" cty:"api_url" hcl:"api_url"`
|
||||
Image *string `mapstructure:"image" required:"true" cty:"image" hcl:"image"`
|
||||
CommercialType *string `mapstructure:"commercial_type" required:"true" cty:"commercial_type" hcl:"commercial_type"`
|
||||
SnapshotName *string `mapstructure:"snapshot_name" required:"false" cty:"snapshot_name" hcl:"snapshot_name"`
|
||||
|
@ -74,6 +76,9 @@ type FlatConfig struct {
|
|||
Bootscript *string `mapstructure:"bootscript" required:"false" cty:"bootscript" hcl:"bootscript"`
|
||||
BootType *string `mapstructure:"boottype" required:"false" cty:"boottype" hcl:"boottype"`
|
||||
RemoveVolume *bool `mapstructure:"remove_volume" cty:"remove_volume" hcl:"remove_volume"`
|
||||
Token *string `mapstructure:"api_token" required:"false" cty:"api_token" hcl:"api_token"`
|
||||
Organization *string `mapstructure:"organization_id" required:"false" cty:"organization_id" hcl:"organization_id"`
|
||||
Region *string `mapstructure:"region" required:"false" cty:"region" hcl:"region"`
|
||||
}
|
||||
|
||||
// FlatMapstructure returns a new FlatConfig.
|
||||
|
@ -142,9 +147,11 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
|
||||
"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},
|
||||
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
|
||||
"organization_id": &hcldec.AttrSpec{Name: "organization_id", Type: cty.String, Required: false},
|
||||
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
|
||||
"access_key": &hcldec.AttrSpec{Name: "access_key", Type: cty.String, Required: false},
|
||||
"secret_key": &hcldec.AttrSpec{Name: "secret_key", Type: cty.String, Required: false},
|
||||
"project_id": &hcldec.AttrSpec{Name: "project_id", Type: cty.String, Required: false},
|
||||
"zone": &hcldec.AttrSpec{Name: "zone", Type: cty.String, Required: false},
|
||||
"api_url": &hcldec.AttrSpec{Name: "api_url", Type: cty.String, Required: false},
|
||||
"image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false},
|
||||
"commercial_type": &hcldec.AttrSpec{Name: "commercial_type", Type: cty.String, Required: false},
|
||||
"snapshot_name": &hcldec.AttrSpec{Name: "snapshot_name", Type: cty.String, Required: false},
|
||||
|
@ -153,6 +160,9 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"bootscript": &hcldec.AttrSpec{Name: "bootscript", Type: cty.String, Required: false},
|
||||
"boottype": &hcldec.AttrSpec{Name: "boottype", Type: cty.String, Required: false},
|
||||
"remove_volume": &hcldec.AttrSpec{Name: "remove_volume", Type: cty.Bool, Required: false},
|
||||
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
|
||||
"organization_id": &hcldec.AttrSpec{Name: "organization_id", Type: cty.String, Required: false},
|
||||
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -7,13 +7,14 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepImage struct{}
|
||||
|
||||
func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
snapshotID := state.Get("snapshot_id").(string)
|
||||
|
@ -21,7 +22,9 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
|
|||
|
||||
ui.Say(fmt.Sprintf("Creating image: %v", c.ImageName))
|
||||
|
||||
image, err := client.GetImage(c.Image)
|
||||
imageResp, err := instanceAPI.GetImage(&instance.GetImageRequest{
|
||||
ImageID: c.Image,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error getting initial image info: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -29,11 +32,16 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if image.DefaultBootscript != nil {
|
||||
bootscriptID = image.DefaultBootscript.Identifier
|
||||
if imageResp.Image.DefaultBootscript != nil {
|
||||
bootscriptID = imageResp.Image.DefaultBootscript.ID
|
||||
}
|
||||
|
||||
imageID, err := client.PostImage(snapshotID, c.ImageName, bootscriptID, image.Arch)
|
||||
createImageResp, err := instanceAPI.CreateImage(&instance.CreateImageRequest{
|
||||
Arch: imageResp.Image.Arch,
|
||||
DefaultBootscript: bootscriptID,
|
||||
Name: c.ImageName,
|
||||
RootVolume: snapshotID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating image: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -41,10 +49,11 @@ func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Image ID: %s", imageID)
|
||||
state.Put("image_id", imageID)
|
||||
log.Printf("Image ID: %s", createImageResp.Image.ID)
|
||||
state.Put("image_id", createImageResp.Image.ID)
|
||||
state.Put("image_name", c.ImageName)
|
||||
state.Put("region", c.Region)
|
||||
state.Put("region", c.Zone) // Deprecated
|
||||
state.Put("zone", c.Zone)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
|
|
@ -7,7 +7,8 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepCreateServer struct {
|
||||
|
@ -15,7 +16,7 @@ type stepCreateServer struct {
|
|||
}
|
||||
|
||||
func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
tags := []string{}
|
||||
|
@ -31,16 +32,16 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
|||
tags = []string{fmt.Sprintf("AUTHORIZED_KEY=%s", strings.Replace(strings.TrimSpace(string(c.Comm.SSHPublicKey)), " ", "_", -1))}
|
||||
}
|
||||
|
||||
server, err := client.PostServer(api.ScalewayServerDefinition{
|
||||
Name: c.ServerName,
|
||||
Image: &c.Image,
|
||||
Organization: c.Organization,
|
||||
CommercialType: c.CommercialType,
|
||||
Tags: tags,
|
||||
Bootscript: bootscript,
|
||||
BootType: c.BootType,
|
||||
})
|
||||
bootType := instance.BootType(c.BootType)
|
||||
|
||||
createServerResp, err := instanceAPI.CreateServer(&instance.CreateServerRequest{
|
||||
BootType: &bootType,
|
||||
Bootscript: bootscript,
|
||||
CommercialType: c.CommercialType,
|
||||
Name: c.ServerName,
|
||||
Image: c.Image,
|
||||
Tags: tags,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating server: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -48,8 +49,10 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
err = client.PostServerAction(server, "poweron")
|
||||
|
||||
_, err = instanceAPI.ServerAction(&instance.ServerActionRequest{
|
||||
Action: instance.ServerActionPoweron,
|
||||
ServerID: createServerResp.Server.ID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error starting server: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -57,9 +60,9 @@ func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) mu
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.serverID = server
|
||||
s.serverID = createServerResp.Server.ID
|
||||
|
||||
state.Put("server_id", server)
|
||||
state.Put("server_id", createServerResp.Server.ID)
|
||||
// instance_id is the generic term used so that users can have access to the
|
||||
// instance id inside of the provisioners, used in step_provision.
|
||||
state.Put("instance_id", s.serverID)
|
||||
|
@ -72,16 +75,22 @@ func (s *stepCreateServer) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
|
||||
ui.Say("Destroying server...")
|
||||
|
||||
err := client.DeleteServerForce(s.serverID)
|
||||
|
||||
err := instanceAPI.DeleteServer(&instance.DeleteServerRequest{
|
||||
ServerID: s.serverID,
|
||||
})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying server. Please destroy it manually: %s", err))
|
||||
_, err = instanceAPI.ServerAction(&instance.ServerActionRequest{
|
||||
Action: instance.ServerActionTerminate,
|
||||
ServerID: s.serverID,
|
||||
})
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error destroying server. Please destroy it manually: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
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) {
|
||||
}
|
|
@ -0,0 +1,154 @@
|
|||
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,10 +4,10 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
|
||||
"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"
|
||||
)
|
||||
|
||||
type stepRemoveVolume struct{}
|
||||
|
@ -24,7 +24,7 @@ func (s *stepRemoveVolume) Cleanup(state multistep.StateBag) {
|
|||
return
|
||||
}
|
||||
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
volumeID := state.Get("root_volume_id").(string)
|
||||
|
@ -35,7 +35,9 @@ func (s *stepRemoveVolume) Cleanup(state multistep.StateBag) {
|
|||
|
||||
ui.Say("Removing Volume ...")
|
||||
|
||||
err := client.DeleteVolume(volumeID)
|
||||
err := instanceAPI.DeleteVolume(&instance.DeleteVolumeRequest{
|
||||
VolumeID: volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error removing volume: %s", err)
|
||||
state.Put("error", err)
|
||||
|
|
|
@ -6,19 +6,22 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepServerInfo struct{}
|
||||
|
||||
func (s *stepServerInfo) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
serverID := state.Get("server_id").(string)
|
||||
|
||||
ui.Say("Waiting for server to become active...")
|
||||
|
||||
_, err := api.WaitForServerState(client, serverID, "running")
|
||||
instanceResp, err := instanceAPI.WaitForServer(&instance.WaitForServerRequest{
|
||||
ServerID: serverID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for server to become booted: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -26,16 +29,22 @@ func (s *stepServerInfo) Run(ctx context.Context, state multistep.StateBag) mult
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
server, err := client.GetServer(serverID)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error retrieving server: %s", err)
|
||||
if instanceResp.State != instance.ServerStateRunning {
|
||||
err := fmt.Errorf("Server is in state %s", instanceResp.State.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("server_ip", server.PublicAddress.IP)
|
||||
state.Put("root_volume_id", server.Volumes["0"].Identifier)
|
||||
if instanceResp.PublicIP == nil {
|
||||
err := fmt.Errorf("Server does not have a public IP")
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state.Put("server_ip", instanceResp.PublicIP.Address.String())
|
||||
state.Put("root_volume_id", instanceResp.Volumes["0"].ID)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
|
|
@ -6,20 +6,23 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
serverID := state.Get("server_id").(string)
|
||||
|
||||
ui.Say("Shutting down server...")
|
||||
|
||||
err := client.PostServerAction(serverID, "poweroff")
|
||||
|
||||
_, err := instanceAPI.ServerAction(&instance.ServerActionRequest{
|
||||
Action: instance.ServerActionPoweroff,
|
||||
ServerID: serverID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error stopping server: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -27,8 +30,9 @@ func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
_, err = api.WaitForServerState(client, serverID, "stopped")
|
||||
|
||||
instanceResp, err := instanceAPI.WaitForServer(&instance.WaitForServerRequest{
|
||||
ServerID: serverID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error shutting down server: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -36,6 +40,13 @@ func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multis
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if instanceResp.State != instance.ServerStateStopped {
|
||||
err := fmt.Errorf("Server is in state %s instead of stopped", instanceResp.State.String())
|
||||
state.Put("error", err)
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
|
|
@ -7,19 +7,23 @@ import (
|
|||
|
||||
"github.com/hashicorp/packer/helper/multistep"
|
||||
"github.com/hashicorp/packer/packer"
|
||||
"github.com/scaleway/scaleway-cli/pkg/api"
|
||||
"github.com/scaleway/scaleway-sdk-go/api/instance/v1"
|
||||
"github.com/scaleway/scaleway-sdk-go/scw"
|
||||
)
|
||||
|
||||
type stepSnapshot struct{}
|
||||
|
||||
func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||
client := state.Get("client").(*api.ScalewayAPI)
|
||||
instanceAPI := instance.NewAPI(state.Get("client").(*scw.Client))
|
||||
ui := state.Get("ui").(packer.Ui)
|
||||
c := state.Get("config").(*Config)
|
||||
volumeID := state.Get("root_volume_id").(string)
|
||||
|
||||
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
|
||||
snapshot, err := client.PostSnapshot(volumeID, c.SnapshotName)
|
||||
createSnapshotResp, err := instanceAPI.CreateSnapshot(&instance.CreateSnapshotRequest{
|
||||
Name: c.SnapshotName,
|
||||
VolumeID: volumeID,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||
state.Put("error", err)
|
||||
|
@ -27,10 +31,11 @@ func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multis
|
|||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Snapshot ID: %s", snapshot)
|
||||
state.Put("snapshot_id", snapshot)
|
||||
log.Printf("Snapshot ID: %s", createSnapshotResp.Snapshot.ID)
|
||||
state.Put("snapshot_id", createSnapshotResp.Snapshot.ID)
|
||||
state.Put("snapshot_name", c.SnapshotName)
|
||||
state.Put("region", c.Region)
|
||||
state.Put("region", c.Zone) // Deprecated
|
||||
state.Put("zone", c.Zone)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ type FlatConfig struct {
|
|||
HTTPPortMin *int `mapstructure:"http_port_min" cty:"http_port_min" hcl:"http_port_min"`
|
||||
HTTPPortMax *int `mapstructure:"http_port_max" cty:"http_port_max" hcl:"http_port_max"`
|
||||
HTTPAddress *string `mapstructure:"http_bind_address" cty:"http_bind_address" hcl:"http_bind_address"`
|
||||
HTTPInterface *string `mapstructure:"http_interface" undocumented:"true" cty:"http_interface" hcl:"http_interface"`
|
||||
ISOChecksum *string `mapstructure:"iso_checksum" required:"true" cty:"iso_checksum" hcl:"iso_checksum"`
|
||||
RawSingleISOUrl *string `mapstructure:"iso_url" required:"true" cty:"iso_url" hcl:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls" cty:"iso_urls" hcl:"iso_urls"`
|
||||
|
@ -125,6 +126,7 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||
"http_port_min": &hcldec.AttrSpec{Name: "http_port_min", Type: cty.Number, Required: false},
|
||||
"http_port_max": &hcldec.AttrSpec{Name: "http_port_max", Type: cty.Number, Required: false},
|
||||
"http_bind_address": &hcldec.AttrSpec{Name: "http_bind_address", Type: cty.String, Required: false},
|
||||
"http_interface": &hcldec.AttrSpec{Name: "http_interface", Type: cty.String, Required: false},
|
||||
"iso_checksum": &hcldec.AttrSpec{Name: "iso_checksum", Type: cty.String, Required: false},
|
||||
"iso_url": &hcldec.AttrSpec{Name: "iso_url", Type: cty.String, Required: false},
|
||||
"iso_urls": &hcldec.AttrSpec{Name: "iso_urls", Type: cty.List(cty.String), Required: false},
|
||||
|
|
|
@ -4,6 +4,7 @@ package common
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/packer/template/interpolate"
|
||||
)
|
||||
|
@ -19,13 +20,36 @@ 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" required:"false"`
|
||||
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"`
|
||||
}
|
||||
|
||||
func (c *GuestAdditionsConfig) Prepare(ctx *interpolate.Context) []error {
|
||||
|
@ -36,5 +60,36 @@ 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
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue