Merge pull request #10 from hashicorp/master

merge with hashicorp/packer master branch
This commit is contained in:
Yu SungDuk 2018-02-13 21:55:50 +09:00 committed by GitHub
commit 5f9718b589
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
364 changed files with 36894 additions and 2022 deletions

View File

@ -1,18 +1,129 @@
## (UNRELEASED)
## UNRELEASED
## 1.2.0 (February 9, 2018)
### BACKWARDS INCOMPATIBILITIES:
* 3rd party plugins: We have moved internal dependencies, meaning your 3rd
party plugins will no longer compile (however existing builds will still
work fine); the work to fix them is minimal and documented in GH-5810.
[GH-5810]
* builder/amazon: The `ssh_private_ip` option has been removed. Instead, please
use `"ssh_interface": "private"`. A fixer has been written for this, which
can be invoked with `packer fix`. [GH-5876]
* builder/openstack: Extension support has been removed. To use OpenStack
builder with the OpenStack Newton (Oct 2016) or earlier, we recommend you
use Packer v1.1.2 or earlier version.
* core: Affects Windows guests: User variables containing Powershell special
characters no longer need to be escaped.[GH-5376]
* provisioner/file: We've made destination semantics more consistent across the
various communicators. In general, if the destination is a directory, files
will be uploaded into the directory instead of failing. This mirrors the
behavior of `rsync`. There's a chance some users might be depending on the
previous buggy behavior, so it's worth ensuring your configuration is
correct. [GH-5426]
* provisioner/powershell: Regression from v1.1.1 forcing extra escaping of
environment variables in the non-elevated provisioner has been fixed.
[GH-5515] [GH-5872]
### IMPROVEMENTS:
* builder/docker: Remove credentials from being shown in the log. [GH-5666]
* builder/triton: Triton RBAC is now supported. [GH-5741]
* provisioner/ansible: Improve user retrieval. [GH-5758]
* post-processor/docker: Remove credentials from being shown in the log. [GH-5666]
* builder/amazon: Warn during prepare if we didn't get both an access key and a secret key when we were expecting one. [GH-5762]
* builder/amazon: Replace `InstanceStatusOK` check with `InstanceReady`. This reduces build times universally while still working for all instance types. [GH-5678]
* **New builder:** `ncloud` for building server images using the NAVER Cloud
Platform. [GH-5791]
* **New builder:** `oci-classic` for building new custom images for use with
Oracle Cloud Infrastructure Classic Compute. [GH-5819]
* **New builder:** `scaleway` - The Scaleway Packer builder is able to create
new images for use with Scaleway BareMetal and Virtual cloud server.
[GH-4770]
* builder/amazon: Add `kms_key_id` option to block device mappings. [GH-5774]
* builder/amazon: Add `skip_metadata_api_check` option to skip consulting the
amazon metadata service. [GH-5764]
* builder/amazon: Add Paris region (eu-west-3) [GH-5718]
* builder/amazon: Give better error messages if we have trouble during
authentication. [GH-5764]
* builder/amazon: Remove Session Token (STS) from being shown in the log.
[GH-5665]
* builder/amazon: Replace `InstanceStatusOK` check with `InstanceReady`. This
reduces build times universally while still working for all instance types.
[GH-5678]
* builder/amazon: Report which authentication provider we're using. [GH-5764]
* builder/amazon: Timeout early if metadata service can't be reached. [GH-5764]
* builder/amazon: Warn during prepare if we didn't get both an access key and a
secret key when we were expecting one. [GH-5762]
* builder/azure: Add validation for incorrect VHD URLs [GH-5695]
* builder/docker: Remove credentials from being shown in the log. [GH-5666]
* builder/google: Support specifying licenses for images. [GH-5842]
* builder/hyper-v: Allow MAC address specification. [GH-5709]
* builder/hyper-v: New option to use differential disks and Inline disk
creation to improve build time and reduce disk usage [GH-5631]
* builder/qemu: Add Intel HAXM support to QEMU builder [GH-5738]
* builder/triton: Triton RBAC is now supported. [GH-5741]
* builder/triton: Updated triton-go dependencies, allowing better error
handling. [GH-5795]
* builder/vmware-iso: Add support for cdrom and disk adapter types. [GH-3417]
* builder/vmware-iso: Add support for setting network type and network adapter
type. [GH-3417]
* builder/vmware-iso: Add support for usb/serial/parallel ports. [GH-3417]
* builder/vmware-iso: Add support for virtual soundcards. [GH-3417]
* builder/vmware-iso: More reliably retrieve the guest networking
configuration. [GH-3417]
* builder/vmware: Add support for "super" key in `boot_command`. [GH-5681]
* communicator/ssh: Add session-level keep-alives [GH-5830]
* communicator/ssh: Detect dead connections. [GH-4709]
* core: Gracefully clean up resources on SIGTERM. [GH-5318]
* core: Improved error logging in floppy file handling. [GH-5802]
* core: Improved support for downloading and validating a uri containing a
Windows UNC path or a relative file:// scheme. [GH-2906]
* post-processor/amazon-import: Allow user to specify role name in amazon-
import [GH-5817]
* post-processor/docker: Remove credentials from being shown in the log.
[GH-5666]
* post-processor/google-export: Synchronize credential semantics with the
Google builder. [GH-4148]
* post-processor/vagrant: Add vagrant post-processor support for Google
[GH-5732]
* post-processor/vsphere-template: Now accepts artifacts from the vSphere post-
processor. [GH-5380]
* provisioner/amazon: Use Amazon SDK's InstanceRunning waiter instead of
InstanceStatusOK waiter [GH-5773]
* provisioner/ansible: Improve user retrieval. [GH-5758]
* provisioner/chef: Add support for 'trusted_certs_dir' chef-client
configuration option [GH-5790]
* provisioner/chef: Added Policyfile support to chef-client provisioner.
[GH-5831]
### BUG FIXES:
* builder/alicloud-ecs: Attach keypair before starting instance in alicloud builder [GH-5739]
* builder/alicloud-ecs: Attach keypair before starting instance in alicloud
builder [GH-5739]
* builder/amazon: Fix tagging support when building in us-gov/china. [GH-5841]
* builder/amazon: NewSession now inherits MaxRetries and other settings.
[GH-5719]
* builder/virtualbox: Fix interpolation ordering so that edge cases around
guest_additions_url are handled correctly [GH-5757]
* builder/virtualbox: Fix regression affecting users running Packer on a
Windows host that kept Packer from finding Virtualbox guest additions if
Packer ran on a different drive from the one where the guest additions were
stored. [GH-5761]
* builder/vmware: Fix case where artifacts might not be cleaned up correctly.
[GH-5835]
* builder/vmware: Fixed file handle leak that may have caused race conditions
in vmware builder [GH-5767]
* communicator/ssh: Add deadline to SSH connection to prevent Packer hangs
after script provisioner reboots vm [GH-4684]
* communicator/winrm: Fix issue copying empty directories. [GH-5763]
* provisioner/ansible-local: Fix support for `--extra-vars` in
`extra_arguments`. [GH-5703]
* provisioner/ansible-remote: Fixes an error where Packer's private key can be
overridden by inherited `ansible_ssh_private_key` options. [GH-5869]
* provisioner/ansible: The "default extra variables" feature added in Packer
v1.0.1 caused the ansible-local provisioner to fail when an --extra-vars
argument was specified in the extra_arguments configuration option; this
has been fixed. [GH-5335]
* provisioner/powershell: Regression from v1.1.1 forcing extra escaping of
environment variables in the non-elevated provisioner has been fixed.
[GH-5515] [GH-5872]
## 1.1.3 (December 8, 2017)

View File

@ -13,6 +13,8 @@
/builder/oracle/ @prydie @owainlewis
/builder/profitbricks/ @jasmingacic
/builder/triton/ @jen20 @sean-
/builder/ncloud/ @YuSungDuk
/builder/scaleway/ @dimtion @edouardb
# provisioners

View File

@ -38,6 +38,7 @@ comes out of the box with support for the following platforms:
* Parallels
* ProfitBricks
* QEMU. Both KVM and Xen images.
* Scaleway
* Triton (Joyent Public Cloud)
* VMware
* VirtualBox

View File

@ -71,11 +71,16 @@ func TestBuilderPrepare_ChrootMounts(t *testing.T) {
if err != nil {
t.Errorf("err: %s", err)
}
}
func TestBuilderPrepare_ChrootMountsBadDefaults(t *testing.T) {
b := &Builder{}
config := testConfig()
config["chroot_mounts"] = [][]string{
{"bad"},
}
warnings, err = b.Prepare(config)
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
@ -135,9 +140,14 @@ func TestBuilderPrepare_CopyFiles(t *testing.T) {
if len(b.config.CopyFiles) != 1 && b.config.CopyFiles[0] != "/etc/resolv.conf" {
t.Errorf("Was expecting default value for copy_files.")
}
}
func TestBuilderPrepare_CopyFilesNoDefault(t *testing.T) {
b := &Builder{}
config := testConfig()
config["copy_files"] = []string{}
warnings, err = b.Prepare(config)
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}

View File

@ -0,0 +1,10 @@
package chroot
import "testing"
func TestDevicePrefixMatch(t *testing.T) {
/*
if devicePrefixMatch("nvme0n1") != "" {
}
*/
}

View File

@ -1,13 +1,17 @@
package common
import (
"errors"
"fmt"
"log"
"os"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
"github.com/aws/aws-sdk-go/aws/ec2metadata"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/hashicorp/go-cleanhttp"
@ -16,15 +20,16 @@ import (
// AccessConfig is for common configuration related to AWS access
type AccessConfig struct {
AccessKey string `mapstructure:"access_key"`
CustomEndpointEc2 string `mapstructure:"custom_endpoint_ec2"`
MFACode string `mapstructure:"mfa_code"`
ProfileName string `mapstructure:"profile"`
RawRegion string `mapstructure:"region"`
SecretKey string `mapstructure:"secret_key"`
SkipValidation bool `mapstructure:"skip_region_validation"`
Token string `mapstructure:"token"`
session *session.Session
AccessKey string `mapstructure:"access_key"`
CustomEndpointEc2 string `mapstructure:"custom_endpoint_ec2"`
MFACode string `mapstructure:"mfa_code"`
ProfileName string `mapstructure:"profile"`
RawRegion string `mapstructure:"region"`
SecretKey string `mapstructure:"secret_key"`
SkipValidation bool `mapstructure:"skip_region_validation"`
SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"`
Token string `mapstructure:"token"`
session *session.Session
}
// Config returns a valid aws.Config object for access to AWS services, or
@ -34,14 +39,78 @@ func (c *AccessConfig) Session() (*session.Session, error) {
return c.session, nil
}
config := aws.NewConfig().WithMaxRetries(11).WithCredentialsChainVerboseErrors(true)
// build a chain provider, lazy-evaluated by aws-sdk
providers := []credentials.Provider{
&credentials.StaticProvider{Value: credentials.Value{
AccessKeyID: c.AccessKey,
SecretAccessKey: c.SecretKey,
SessionToken: c.Token,
}},
&credentials.EnvProvider{},
&credentials.SharedCredentialsProvider{
Filename: "",
Profile: c.ProfileName,
},
}
if c.ProfileName != "" {
if err := os.Setenv("AWS_PROFILE", c.ProfileName); err != nil {
return nil, fmt.Errorf("Set env error: %s", err)
// Build isolated HTTP client to avoid issues with globally-shared settings
client := cleanhttp.DefaultClient()
// Keep the default timeout (100ms) low as we don't want to wait in non-EC2 environments
client.Timeout = 100 * time.Millisecond
const userTimeoutEnvVar = "AWS_METADATA_TIMEOUT"
userTimeout := os.Getenv(userTimeoutEnvVar)
if userTimeout != "" {
newTimeout, err := time.ParseDuration(userTimeout)
if err == nil {
if newTimeout.Nanoseconds() > 0 {
client.Timeout = newTimeout
} else {
log.Printf("[WARN] Non-positive value of %s (%s) is meaningless, ignoring", userTimeoutEnvVar, newTimeout.String())
}
} else {
log.Printf("[WARN] Error converting %s to time.Duration: %s", userTimeoutEnvVar, err)
}
}
log.Printf("[INFO] Setting AWS metadata API timeout to %s", client.Timeout.String())
cfg := &aws.Config{
HTTPClient: client,
}
if !c.SkipMetadataApiCheck {
// Real AWS should reply to a simple metadata request.
// We check it actually does to ensure something else didn't just
// happen to be listening on the same IP:Port
metadataClient := ec2metadata.New(session.New(cfg))
if metadataClient.Available() {
providers = append(providers, &ec2rolecreds.EC2RoleProvider{
Client: metadataClient,
})
log.Print("[INFO] AWS EC2 instance detected via default metadata" +
" API endpoint, EC2RoleProvider added to the auth chain")
} else {
log.Printf("[INFO] Ignoring AWS metadata API endpoint " +
"as it doesn't return any instance-id")
}
}
creds := credentials.NewChainCredentials(providers)
cp, err := creds.Get()
if err != nil {
if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "NoCredentialProviders" {
return nil, errors.New("No valid credential sources found for AWS Builder. " +
"Please see https://www.packer.io/docs/builders/amazon.html#specifying-amazon-credentials " +
"for more information on providing credentials for the AWS Builder.")
}
return nil, fmt.Errorf("Error loading credentials for AWS Provider: %s", err)
}
log.Printf("[INFO] AWS Auth provider used: %q", cp.ProviderName)
config := aws.NewConfig().WithMaxRetries(11).WithCredentialsChainVerboseErrors(true)
config = config.WithCredentials(creds)
if c.RawRegion != "" {
config = config.WithRegion(c.RawRegion)
} else if region := c.metadataRegion(); region != "" {
@ -52,11 +121,6 @@ func (c *AccessConfig) Session() (*session.Session, error) {
config = config.WithEndpoint(c.CustomEndpointEc2)
}
if c.AccessKey != "" {
config = config.WithCredentials(
credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token))
}
opts := session.Options{
SharedConfigState: session.SharedConfigEnable,
Config: *config,
@ -80,6 +144,21 @@ func (c *AccessConfig) Session() (*session.Session, error) {
return c.session, nil
}
func (c *AccessConfig) SessionRegion() string {
if c.session == nil {
panic("access config session should be set.")
}
return aws.StringValue(c.session.Config.Region)
}
func (c *AccessConfig) IsGovCloud() bool {
return strings.HasPrefix(c.SessionRegion(), "us-gov-")
}
func (c *AccessConfig) IsChinaCloud() bool {
return strings.HasPrefix(c.SessionRegion(), "cn-")
}
// metadataRegion returns the region from the metadata service
func (c *AccessConfig) metadataRegion() string {

View File

@ -2,6 +2,9 @@ package common
import (
"testing"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
)
func testAccessConfig() *AccessConfig {
@ -38,3 +41,20 @@ func TestAccessConfigPrepare_Region(t *testing.T) {
c.SkipValidation = false
}
func TestAccessConfigPrepare_RegionRestrictd(t *testing.T) {
c := testAccessConfig()
// Create a Session with a custom region
c.session = session.Must(session.NewSession(&aws.Config{
Region: aws.String("us-gov-west-1"),
}))
if err := c.Prepare(nil); err != nil {
t.Fatalf("shouldn't have err: %s", err)
}
if !c.IsGovCloud() {
t.Fatal("We should be in gov region.")
}
}

View File

@ -17,7 +17,7 @@ type AMIConfig struct {
AMIProductCodes []string `mapstructure:"ami_product_codes"`
AMIRegions []string `mapstructure:"ami_regions"`
AMISkipRegionValidation bool `mapstructure:"skip_region_validation"`
AMITags map[string]string `mapstructure:"tags"`
AMITags TagMap `mapstructure:"tags"`
AMIENASupport bool `mapstructure:"ena_support"`
AMISriovNetSupport bool `mapstructure:"sriov_support"`
AMIForceDeregister bool `mapstructure:"force_deregister"`
@ -25,7 +25,7 @@ type AMIConfig struct {
AMIEncryptBootVolume bool `mapstructure:"encrypt_boot"`
AMIKmsKeyId string `mapstructure:"kms_key_id"`
AMIRegionKMSKeyIDs map[string]string `mapstructure:"region_kms_key_ids"`
SnapshotTags map[string]string `mapstructure:"snapshot_tags"`
SnapshotTags TagMap `mapstructure:"snapshot_tags"`
SnapshotUsers []string `mapstructure:"snapshot_users"`
SnapshotGroups []string `mapstructure:"snapshot_groups"`
}

View File

@ -53,7 +53,6 @@ type RunConfig struct {
// Communicator settings
Comm communicator.Config `mapstructure:",squash"`
SSHKeyPairName string `mapstructure:"ssh_keypair_name"`
SSHPrivateIp bool `mapstructure:"ssh_private_ip"`
SSHInterface string `mapstructure:"ssh_interface"`
}
@ -78,14 +77,6 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
// Validation
errs := c.Comm.Prepare(ctx)
if c.SSHPrivateIp && c.SSHInterface != "" {
errs = append(errs, errors.New("ssh_interface and ssh_private_ip should not both be specified"))
}
// Legacy configurable
if c.SSHPrivateIp {
c.SSHInterface = "private_ip"
}
// Valadating ssh_interface
if c.SSHInterface != "public_ip" &&

View File

@ -15,8 +15,8 @@ import (
)
type StepCreateTags struct {
Tags map[string]string
SnapshotTags map[string]string
Tags TagMap
SnapshotTags TagMap
Ctx interpolate.Context
}
@ -33,7 +33,7 @@ func (s *StepCreateTags) Run(_ context.Context, state multistep.StateBag) multis
sourceAMI = ""
}
if len(s.Tags) == 0 && len(s.SnapshotTags) == 0 {
if !s.Tags.IsSet() && !s.SnapshotTags.IsSet() {
return multistep.ActionContinue
}
@ -79,22 +79,22 @@ func (s *StepCreateTags) Run(_ context.Context, state multistep.StateBag) multis
// Convert tags to ec2.Tag format
ui.Say("Creating AMI tags")
amiTags, err := ConvertToEC2Tags(s.Tags, *ec2conn.Config.Region, sourceAMI, s.Ctx)
amiTags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, sourceAMI)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ReportTags(ui, amiTags)
amiTags.Report(ui)
ui.Say("Creating snapshot tags")
snapshotTags, err := ConvertToEC2Tags(s.SnapshotTags, *ec2conn.Config.Region, sourceAMI, s.Ctx)
snapshotTags, err := s.SnapshotTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, sourceAMI)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ReportTags(ui, snapshotTags)
snapshotTags.Report(ui)
// Retry creating tags for about 2.5 minutes
err = retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
@ -142,36 +142,3 @@ func (s *StepCreateTags) Run(_ context.Context, state multistep.StateBag) multis
func (s *StepCreateTags) Cleanup(state multistep.StateBag) {
// No cleanup...
}
func ReportTags(ui packer.Ui, tags []*ec2.Tag) {
for _, tag := range tags {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"",
aws.StringValue(tag.Key), aws.StringValue(tag.Value)))
}
}
func ConvertToEC2Tags(tags map[string]string, region, sourceAmiId string, ctx interpolate.Context) ([]*ec2.Tag, error) {
var ec2Tags []*ec2.Tag
for key, value := range tags {
ctx.Data = &BuildInfoTemplate{
SourceAMI: sourceAmiId,
BuildRegion: region,
}
interpolatedKey, err := interpolate.Render(key, &ctx)
if err != nil {
return ec2Tags, fmt.Errorf("Error processing tag: %s:%s - %s", key, value, err)
}
interpolatedValue, err := interpolate.Render(value, &ctx)
if err != nil {
return ec2Tags, fmt.Errorf("Error processing tag: %s:%s - %s", key, value, err)
}
ec2Tags = append(ec2Tags, &ec2.Tag{
Key: aws.String(interpolatedKey),
Value: aws.String(interpolatedValue),
})
}
return ec2Tags, nil
}

View File

@ -8,8 +8,10 @@ import (
"log"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
retry "github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
@ -19,19 +21,20 @@ type StepRunSourceInstance struct {
AssociatePublicIpAddress bool
AvailabilityZone string
BlockDevices BlockDevices
Ctx interpolate.Context
Debug bool
EbsOptimized bool
ExpectedRootDevice string
IamInstanceProfile string
InstanceInitiatedShutdownBehavior string
InstanceType string
IsRestricted bool
SourceAMI string
SubnetId string
Tags map[string]string
VolumeTags map[string]string
Tags TagMap
UserData string
UserDataFile string
Ctx interpolate.Context
VolumeTags TagMap
instanceId string
}
@ -85,16 +88,15 @@ func (s *StepRunSourceInstance) Run(_ context.Context, state multistep.StateBag)
s.Tags["Name"] = "Packer Builder"
}
ec2Tags, err := ConvertToEC2Tags(s.Tags, *ec2conn.Config.Region, s.SourceAMI, s.Ctx)
ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, s.SourceAMI)
if err != nil {
err := fmt.Errorf("Error tagging source instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ReportTags(ui, ec2Tags)
volTags, err := ConvertToEC2Tags(s.VolumeTags, *ec2conn.Config.Region, s.SourceAMI, s.Ctx)
volTags, err := s.VolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, s.SourceAMI)
if err != nil {
err := fmt.Errorf("Error tagging volumes: %s", err)
state.Put("error", err)
@ -114,6 +116,7 @@ func (s *StepRunSourceInstance) Run(_ context.Context, state multistep.StateBag)
EbsOptimized: &s.EbsOptimized,
}
// Collect tags for tagging on resource creation
var tagSpecs []*ec2.TagSpecification
if len(ec2Tags) > 0 {
@ -134,8 +137,11 @@ func (s *StepRunSourceInstance) Run(_ context.Context, state multistep.StateBag)
tagSpecs = append(tagSpecs, runVolTags)
}
if len(tagSpecs) > 0 {
// If our region supports it, set tag specifications
if len(tagSpecs) > 0 && !s.IsRestricted {
runOpts.SetTagSpecifications(tagSpecs)
ec2Tags.Report(ui)
volTags.Report(ui)
}
if keyName != "" {
@ -212,6 +218,70 @@ func (s *StepRunSourceInstance) Run(_ context.Context, state multistep.StateBag)
state.Put("instance", instance)
// If we're in a region that doesn't support tagging on instance creation,
// do that now.
if s.IsRestricted {
ec2Tags.Report(ui)
// Retry creating tags for about 2.5 minutes
err = retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
_, err := ec2conn.CreateTags(&ec2.CreateTagsInput{
Tags: ec2Tags,
Resources: []*string{instance.InstanceId},
})
if err == nil {
return true, nil
}
if awsErr, ok := err.(awserr.Error); ok {
if awsErr.Code() == "InvalidInstanceID.NotFound" {
return false, nil
}
}
return true, err
})
if err != nil {
err := fmt.Errorf("Error tagging source instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Now tag volumes
volumeIds := make([]*string, 0)
for _, v := range instance.BlockDeviceMappings {
if ebs := v.Ebs; ebs != nil {
volumeIds = append(volumeIds, ebs.VolumeId)
}
}
if len(volumeIds) > 0 && s.VolumeTags.IsSet() {
ui.Say("Adding tags to source EBS Volumes")
volumeTags, err := s.VolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, s.SourceAMI)
if err != nil {
err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
volumeTags.Report(ui)
_, err = ec2conn.CreateTags(&ec2.CreateTagsInput{
Resources: volumeIds,
Tags: volumeTags,
})
if err != nil {
err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}

View File

@ -33,8 +33,8 @@ type StepRunSpotInstance struct {
SpotPrice string
SpotPriceProduct string
SubnetId string
Tags map[string]string
VolumeTags map[string]string
Tags TagMap
VolumeTags TagMap
UserData string
UserDataFile string
Ctx interpolate.Context
@ -143,14 +143,14 @@ func (s *StepRunSpotInstance) Run(_ context.Context, state multistep.StateBag) m
s.Tags["Name"] = "Packer Builder"
}
ec2Tags, err := ConvertToEC2Tags(s.Tags, *ec2conn.Config.Region, s.SourceAMI, s.Ctx)
ec2Tags, err := s.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, s.SourceAMI)
if err != nil {
err := fmt.Errorf("Error tagging source instance: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ReportTags(ui, ec2Tags)
ec2Tags.Report(ui)
ui.Message(fmt.Sprintf(
"Requesting spot instance '%s' for: %s",
@ -284,21 +284,21 @@ func (s *StepRunSpotInstance) Run(_ context.Context, state multistep.StateBag) m
}
}
if len(volumeIds) > 0 && len(s.VolumeTags) > 0 {
if len(volumeIds) > 0 && s.VolumeTags.IsSet() {
ui.Say("Adding tags to source EBS Volumes")
tags, err := ConvertToEC2Tags(s.VolumeTags, *ec2conn.Config.Region, s.SourceAMI, s.Ctx)
volumeTags, err := s.VolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, s.SourceAMI)
if err != nil {
err := fmt.Errorf("Error tagging source EBS Volumes on %s: %s", *instance.InstanceId, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ReportTags(ui, tags)
volumeTags.Report(ui)
_, err = ec2conn.CreateTags(&ec2.CreateTagsInput{
Resources: volumeIds,
Tags: tags,
Tags: volumeTags,
})
if err != nil {

View File

@ -0,0 +1,47 @@
package common
import (
"fmt"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type TagMap map[string]string
type EC2Tags []*ec2.Tag
func (t EC2Tags) Report(ui packer.Ui) {
for _, tag := range t {
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"",
aws.StringValue(tag.Key), aws.StringValue(tag.Value)))
}
}
func (t TagMap) IsSet() bool {
return len(t) > 0
}
func (t TagMap) EC2Tags(ctx interpolate.Context, region, sourceAMIID string) (EC2Tags, error) {
var ec2Tags []*ec2.Tag
ctx.Data = &BuildInfoTemplate{
SourceAMI: sourceAMIID,
BuildRegion: region,
}
for key, value := range t {
interpolatedKey, err := interpolate.Render(key, &ctx)
if err != nil {
return nil, fmt.Errorf("Error processing tag: %s:%s - %s", key, value, err)
}
interpolatedValue, err := interpolate.Render(value, &ctx)
if err != nil {
return nil, fmt.Errorf("Error processing tag: %s:%s - %s", key, value, err)
}
ec2Tags = append(ec2Tags, &ec2.Tag{
Key: aws.String(interpolatedKey),
Value: aws.String(interpolatedValue),
})
}
return ec2Tags, nil
}

View File

@ -28,7 +28,7 @@ type Config struct {
awscommon.AMIConfig `mapstructure:",squash"`
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.RunConfig `mapstructure:",squash"`
VolumeRunTags map[string]string `mapstructure:"run_volume_tags"`
VolumeRunTags awscommon.TagMap `mapstructure:"run_volume_tags"`
ctx interpolate.Context
}
@ -152,6 +152,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior,
InstanceType: b.config.InstanceType,
IsRestricted: b.config.IsChinaCloud() || b.config.IsGovCloud(),
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,

View File

@ -26,8 +26,8 @@ type Config struct {
awscommon.BlockDevices `mapstructure:",squash"`
awscommon.AMIConfig `mapstructure:",squash"`
RootDevice RootBlockDevice `mapstructure:"ami_root_device"`
VolumeRunTags map[string]string `mapstructure:"run_volume_tags"`
RootDevice RootBlockDevice `mapstructure:"ami_root_device"`
VolumeRunTags awscommon.TagMap `mapstructure:"run_volume_tags"`
ctx interpolate.Context
}
@ -166,6 +166,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior,
InstanceType: b.config.InstanceType,
IsRestricted: b.config.IsChinaCloud() || b.config.IsGovCloud(),
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,

View File

@ -7,7 +7,7 @@ import (
type BlockDevice struct {
awscommon.BlockDevice `mapstructure:"-,squash"`
Tags map[string]string `mapstructure:"tags"`
Tags awscommon.TagMap `mapstructure:"tags"`
}
func commonBlockDevices(mappings []BlockDevice, ctx *interpolate.Context) (awscommon.BlockDevices, error) {

View File

@ -149,6 +149,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceInitiatedShutdownBehavior: b.config.InstanceInitiatedShutdownBehavior,
InstanceType: b.config.InstanceType,
IsRestricted: b.config.IsChinaCloud() || b.config.IsGovCloud(),
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,

View File

@ -5,7 +5,6 @@ import (
"fmt"
"github.com/aws/aws-sdk-go/service/ec2"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
@ -44,14 +43,14 @@ func (s *stepTagEBSVolumes) Run(_ context.Context, state multistep.StateBag) mul
continue
}
tags, err := awscommon.ConvertToEC2Tags(mapping.Tags, *ec2conn.Config.Region, *sourceAMI.ImageId, s.Ctx)
tags, err := mapping.Tags.EC2Tags(s.Ctx, *ec2conn.Config.Region, *sourceAMI.ImageId)
if err != nil {
err := fmt.Errorf("Error tagging device %s with %s", mapping.DeviceName, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
awscommon.ReportTags(ui, tags)
tags.Report(ui)
for _, v := range instance.BlockDeviceMappings {
if *v.DeviceName == mapping.DeviceName {

View File

@ -232,6 +232,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
EbsOptimized: b.config.EbsOptimized,
IamInstanceProfile: b.config.IamInstanceProfile,
InstanceType: b.config.InstanceType,
IsRestricted: b.config.IsChinaCloud() || b.config.IsGovCloud(),
SourceAMI: b.config.SourceAmi,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,

View File

@ -105,6 +105,21 @@ func (c *Communicator) uploadReader(dst string, src io.Reader) error {
// uploadFile uses docker cp to copy the file from the host to the container
func (c *Communicator) uploadFile(dst string, src io.Reader, fi *os.FileInfo) error {
// find out if it's a directory
testDirectoryCommand := fmt.Sprintf(`test -d "%s"`, dst)
cmd := &packer.RemoteCmd{Command: testDirectoryCommand}
err := c.Start(cmd)
if err != nil {
log.Printf("Unable to check whether remote path is a dir: %s", err)
return err
}
cmd.Wait()
if cmd.ExitStatus == 0 {
log.Printf("path is a directory; copying file into directory.")
dst = filepath.Join(dst, filepath.Base((*fi).Name()))
}
// command format: docker cp /path/to/infile containerid:/path/to/outfile
log.Printf("Copying to %s on container %s.", dst, c.ContainerID)

View File

@ -36,6 +36,7 @@ type Config struct {
ImageDescription string `mapstructure:"image_description"`
ImageFamily string `mapstructure:"image_family"`
ImageLabels map[string]string `mapstructure:"image_labels"`
ImageLicenses []string `mapstructure:"image_licenses"`
InstanceName string `mapstructure:"instance_name"`
Labels map[string]string `mapstructure:"labels"`
MachineType string `mapstructure:"machine_type"`

View File

@ -309,6 +309,9 @@ func testConfig(t *testing.T) map[string]interface{} {
"label-1": "value-1",
"label-2": "value-2",
},
"image_licenses": []string{
"test-license",
},
"zone": "us-east1-a",
}
}

View File

@ -11,7 +11,7 @@ import (
type Driver interface {
// CreateImage creates an image from the given disk in Google Compute
// Engine.
CreateImage(name, description, family, zone, disk string, image_labels map[string]string) (<-chan *Image, <-chan error)
CreateImage(name, description, family, zone, disk string, image_labels map[string]string, image_licenses []string) (<-chan *Image, <-chan error)
// DeleteImage deletes the image with the given name.
DeleteImage(name string) <-chan error

View File

@ -97,12 +97,13 @@ func NewDriverGCE(ui packer.Ui, p string, a *AccountFile) (Driver, error) {
}, nil
}
func (d *driverGCE) CreateImage(name, description, family, zone, disk string, image_labels map[string]string) (<-chan *Image, <-chan error) {
func (d *driverGCE) CreateImage(name, description, family, zone, disk string, image_labels map[string]string, image_licenses []string) (<-chan *Image, <-chan error) {
gce_image := &compute.Image{
Description: description,
Name: name,
Family: family,
Labels: image_labels,
Licenses: image_licenses,
SourceDisk: fmt.Sprintf("%s%s/zones/%s/disks/%s", d.service.BasePath, d.projectId, zone, disk),
SourceType: "RAW",
}

View File

@ -9,9 +9,9 @@ type DriverMock struct {
CreateImageDesc string
CreateImageFamily string
CreateImageLabels map[string]string
CreateImageLicenses []string
CreateImageZone string
CreateImageDisk string
CreateImageResultLicenses []string
CreateImageResultProjectId string
CreateImageResultSelfLink string
CreateImageResultSizeGb int64
@ -82,11 +82,12 @@ type DriverMock struct {
WaitForInstanceErrCh <-chan error
}
func (d *DriverMock) CreateImage(name, description, family, zone, disk string, image_labels map[string]string) (<-chan *Image, <-chan error) {
func (d *DriverMock) CreateImage(name, description, family, zone, disk string, image_labels map[string]string, image_licenses []string) (<-chan *Image, <-chan error) {
d.CreateImageName = name
d.CreateImageDesc = description
d.CreateImageFamily = family
d.CreateImageLabels = image_labels
d.CreateImageLicenses = image_licenses
d.CreateImageZone = zone
d.CreateImageDisk = disk
if d.CreateImageResultProjectId == "" {
@ -106,7 +107,7 @@ func (d *DriverMock) CreateImage(name, description, family, zone, disk string, i
ch := make(chan *Image, 1)
ch <- &Image{
Labels: d.CreateImageLabels,
Licenses: d.CreateImageResultLicenses,
Licenses: d.CreateImageLicenses,
Name: name,
ProjectId: d.CreateImageResultProjectId,
SelfLink: d.CreateImageResultSelfLink,

View File

@ -40,7 +40,7 @@ func (s *StepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
imageCh, errCh := driver.CreateImage(
config.ImageName, config.ImageDescription, config.ImageFamily, config.Zone,
config.DiskName, config.ImageLabels)
config.DiskName, config.ImageLabels, config.ImageLicenses)
var err error
select {
case err = <-errCh:

View File

@ -22,7 +22,6 @@ func TestStepCreateImage(t *testing.T) {
d := state.Get("driver").(*DriverMock)
// These are the values of the image the driver will return.
d.CreateImageResultLicenses = []string{"test-license"}
d.CreateImageResultProjectId = "test-project"
d.CreateImageResultSizeGb = 100
@ -36,7 +35,6 @@ func TestStepCreateImage(t *testing.T) {
assert.True(t, ok, "Image in state is not an Image.")
// Verify created Image results.
assert.Equal(t, image.Licenses, d.CreateImageResultLicenses, "Created image licenses don't match the licenses returned by the driver.")
assert.Equal(t, image.Name, c.ImageName, "Created image does not match config name.")
assert.Equal(t, image.ProjectId, d.CreateImageResultProjectId, "Created image project does not match driver project.")
assert.Equal(t, image.SizeGb, d.CreateImageResultSizeGb, "Created image size does not match the size returned by the driver.")
@ -48,6 +46,7 @@ func TestStepCreateImage(t *testing.T) {
assert.Equal(t, d.CreateImageZone, c.Zone, "Incorrect image zone passed to driver.")
assert.Equal(t, d.CreateImageDisk, c.DiskName, "Incorrect disk passed to driver.")
assert.Equal(t, d.CreateImageLabels, c.ImageLabels, "Incorrect image_labels passed to driver.")
assert.Equal(t, d.CreateImageLicenses, c.ImageLicenses, "Incorrect image_licenses passed to driver.")
}
func TestStepCreateImage_errorOnChannel(t *testing.T) {

View File

@ -52,6 +52,8 @@ type Driver interface {
//Set the vlan to use for machine
SetVirtualMachineVlanId(string, string) error
SetVmNetworkAdapterMacAddress(string, string) error
UntagVirtualMachineNetworkAdapterVlan(string, string) error
CreateExternalVirtualSwitch(string, string) error

View File

@ -67,6 +67,11 @@ type DriverMock struct {
SetNetworkAdapterVlanId_VlanId string
SetNetworkAdapterVlanId_Err error
SetVmNetworkAdapterMacAddress_Called bool
SetVmNetworkAdapterMacAddress_VmName string
SetVmNetworkAdapterMacAddress_Mac string
SetVmNetworkAdapterMacAddress_Err error
SetVirtualMachineVlanId_Called bool
SetVirtualMachineVlanId_VmName string
SetVirtualMachineVlanId_VlanId string
@ -318,6 +323,13 @@ func (d *DriverMock) SetNetworkAdapterVlanId(switchName string, vlanId string) e
return d.SetNetworkAdapterVlanId_Err
}
func (d *DriverMock) SetVmNetworkAdapterMacAddress(vmName string, mac string) error {
d.SetVmNetworkAdapterMacAddress_Called = true
d.SetVmNetworkAdapterMacAddress_VmName = vmName
d.SetVmNetworkAdapterMacAddress_Mac = mac
return d.SetVmNetworkAdapterMacAddress_Err
}
func (d *DriverMock) SetVirtualMachineVlanId(vmName string, vlanId string) error {
d.SetVirtualMachineVlanId_Called = true
d.SetVirtualMachineVlanId_VmName = vmName

View File

@ -146,6 +146,10 @@ func (d *HypervPS4Driver) SetVirtualMachineVlanId(vmName string, vlanId string)
return hyperv.SetVirtualMachineVlanId(vmName, vlanId)
}
func (d *HypervPS4Driver) SetVmNetworkAdapterMacAddress(vmName string, mac string) error {
return hyperv.SetVmNetworkAdapterMacAddress(vmName, mac)
}
func (d *HypervPS4Driver) UntagVirtualMachineNetworkAdapterVlan(vmName string, switchName string) error {
return hyperv.UntagVirtualMachineNetworkAdapterVlan(vmName, switchName)
}

View File

@ -28,6 +28,7 @@ type StepCloneVM struct {
EnableDynamicMemory bool
EnableSecureBoot bool
EnableVirtualizationExtensions bool
MacAddress string
}
func (s *StepCloneVM) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -118,6 +119,16 @@ func (s *StepCloneVM) Run(_ context.Context, state multistep.StateBag) multistep
}
}
if s.MacAddress != "" {
err = driver.SetVmNetworkAdapterMacAddress(s.VMName, s.MacAddress)
if err != nil {
err := fmt.Errorf("Error setting MAC address: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Set the final name in the state bag so others can use it
state.Put("vmName", s.VMName)

View File

@ -29,6 +29,9 @@ type StepCreateVM struct {
EnableVirtualizationExtensions bool
AdditionalDiskSize []uint
DifferencingDisk bool
MacAddress string
SkipExport bool
OutputDir string
}
func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -52,6 +55,12 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
}
vhdPath := state.Get("packerVhdTempDir").(string)
// inline vhd path if export is skipped
if s.SkipExport {
vhdPath = filepath.Join(s.OutputDir, "Virtual Hard Disks")
}
// convert the MB to bytes
ramSize := int64(s.RamSize * 1024 * 1024)
diskSize := int64(s.DiskSize * 1024 * 1024)
@ -125,6 +134,16 @@ func (s *StepCreateVM) Run(_ context.Context, state multistep.StateBag) multiste
}
}
if s.MacAddress != "" {
err = driver.SetVmNetworkAdapterMacAddress(s.VMName, s.MacAddress)
if err != nil {
err := fmt.Errorf("Error setting MAC address: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Set the final name in the state bag so others can use it
state.Put("vmName", s.VMName)

View File

@ -18,18 +18,19 @@ const (
type StepExportVm struct {
OutputDir string
SkipCompaction bool
SkipExport bool
}
func (s *StepExportVm) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
driver := state.Get("driver").(Driver)
ui := state.Get("ui").(packer.Ui)
var err error
var errorMsg string
vmName := state.Get("vmName").(string)
tmpPath := state.Get("packerTempDir").(string)
outputPath := s.OutputDir
expPath := s.OutputDir
// create temp path to export vm
errorMsg = "Error creating temp export path: %s"
@ -40,21 +41,21 @@ func (s *StepExportVm) Run(_ context.Context, state multistep.StateBag) multiste
ui.Error(err.Error())
return multistep.ActionHalt
}
if !s.SkipExport {
ui.Say("Exporting vm...")
ui.Say("Exporting vm...")
err = driver.ExportVirtualMachine(vmName, vmExportPath)
if err != nil {
errorMsg = "Error exporting vm: %s"
err := fmt.Errorf(errorMsg, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
err = driver.ExportVirtualMachine(vmName, vmExportPath)
if err != nil {
errorMsg = "Error exporting vm: %s"
err := fmt.Errorf(errorMsg, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// copy to output dir
expPath = filepath.Join(vmExportPath, vmName)
}
// copy to output dir
expPath := filepath.Join(vmExportPath, vmName)
if s.SkipCompaction {
ui.Say("Skipping disk compaction...")
} else {
@ -69,16 +70,17 @@ func (s *StepExportVm) Run(_ context.Context, state multistep.StateBag) multiste
}
}
ui.Say("Copying to output dir...")
err = driver.CopyExportedVirtualMachine(expPath, outputPath, vhdDir, vmDir)
if err != nil {
errorMsg = "Error exporting vm: %s"
err := fmt.Errorf(errorMsg, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
if !s.SkipExport {
ui.Say("Copying to output dir...")
err = driver.CopyExportedVirtualMachine(expPath, outputPath, vhdDir, vmDir)
if err != nil {
errorMsg = "Error exporting vm: %s"
err := fmt.Errorf(errorMsg, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
return multistep.ActionContinue
}

View File

@ -74,6 +74,7 @@ type Config struct {
BootCommand []string `mapstructure:"boot_command"`
SwitchName string `mapstructure:"switch_name"`
SwitchVlanId string `mapstructure:"switch_vlan_id"`
MacAddress string `mapstructure:"mac_address"`
VlanId string `mapstructure:"vlan_id"`
Cpu uint `mapstructure:"cpu"`
Generation uint `mapstructure:"generation"`
@ -94,6 +95,8 @@ type Config struct {
SkipCompaction bool `mapstructure:"skip_compaction"`
SkipExport bool `mapstructure:"skip_export"`
// Use differencing disk
DifferencingDisk bool `mapstructure:"differencing_disk"`
@ -357,6 +360,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
AdditionalDiskSize: b.config.AdditionalDiskSize,
DifferencingDisk: b.config.DifferencingDisk,
SkipExport: b.config.SkipExport,
OutputDir: b.config.OutputDir,
},
&hypervcommon.StepEnableIntegrationService{},
@ -422,6 +427,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&hypervcommon.StepExportVm{
OutputDir: b.config.OutputDir,
SkipCompaction: b.config.SkipCompaction,
SkipExport: b.config.SkipExport,
},
// the clean up actions for each step will be executed reverse order

View File

@ -82,6 +82,7 @@ type Config struct {
BootCommand []string `mapstructure:"boot_command"`
SwitchName string `mapstructure:"switch_name"`
SwitchVlanId string `mapstructure:"switch_vlan_id"`
MacAddress string `mapstructure:"mac_address"`
VlanId string `mapstructure:"vlan_id"`
Cpu uint `mapstructure:"cpu"`
Generation uint
@ -94,6 +95,8 @@ type Config struct {
SkipCompaction bool `mapstructure:"skip_compaction"`
SkipExport bool `mapstructure:"skip_export"`
ctx interpolate.Context
}
@ -403,6 +406,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
EnableDynamicMemory: b.config.EnableDynamicMemory,
EnableSecureBoot: b.config.EnableSecureBoot,
EnableVirtualizationExtensions: b.config.EnableVirtualizationExtensions,
MacAddress: b.config.MacAddress,
},
&hypervcommon.StepEnableIntegrationService{},
@ -469,6 +473,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&hypervcommon.StepExportVm{
OutputDir: b.config.OutputDir,
SkipCompaction: b.config.SkipCompaction,
SkipExport: b.config.SkipExport,
},
// the clean up actions for each step will be executed reverse order

View File

@ -73,6 +73,24 @@ func (c *LxcAttachCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo)
return err
}
if fi != nil {
tfDir := filepath.Dir(tf.Name())
// rename tempfile to match original file name. This makes sure that if file is being
// moved into a directory, the filename is preserved instead of a temp name.
adjustedTempName := filepath.Join(tfDir, (*fi).Name())
mvCmd, err := c.CmdWrapper(fmt.Sprintf("sudo mv %s %s", tf.Name(), adjustedTempName))
if err != nil {
return err
}
defer os.Remove(adjustedTempName)
ShellCommand(mvCmd).Run()
// change cpCmd to use new file name as source
cpCmd, err = c.CmdWrapper(fmt.Sprintf("sudo cp %s %s", adjustedTempName, dst))
if err != nil {
return err
}
}
log.Printf("Running copy command: %s", dst)
return ShellCommand(cpCmd).Run()

View File

@ -55,7 +55,24 @@ func (c *Communicator) Start(cmd *packer.RemoteCmd) error {
}
func (c *Communicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
cpCmd, err := c.CmdWrapper(fmt.Sprintf("lxc file push - %s", filepath.Join(c.ContainerName, dst)))
fileDestination := filepath.Join(c.ContainerName, dst)
// find out if the place we are pushing to is a directory
testDirectoryCommand := fmt.Sprintf(`test -d "%s"`, dst)
cmd := &packer.RemoteCmd{Command: testDirectoryCommand}
err := c.Start(cmd)
if err != nil {
log.Printf("Unable to check whether remote path is a dir: %s", err)
return err
}
cmd.Wait()
if cmd.ExitStatus == 0 {
log.Printf("path is a directory; copying file into directory.")
fileDestination = filepath.Join(c.ContainerName, dst, (*fi).Name())
}
cpCmd, err := c.CmdWrapper(fmt.Sprintf("lxc file push - %s", fileDestination))
if err != nil {
return err
}

1
builder/openstack/builder.go Normal file → Executable file
View File

@ -70,7 +70,6 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&StepLoadExtensions{},
&StepLoadFlavor{
Flavor: b.config.Flavor,
},

View File

@ -1,59 +0,0 @@
package openstack
import (
"context"
"fmt"
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions"
"github.com/gophercloud/gophercloud/pagination"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// StepLoadExtensions gets the FlavorRef from a Flavor. It first assumes
// that the Flavor is a ref and verifies it. Otherwise, it tries to find
// the flavor by name.
type StepLoadExtensions struct{}
func (s *StepLoadExtensions) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
ui.Say("Discovering enabled extensions...")
result := make(map[string]struct{}, 15)
pager := extensions.List(client)
err = pager.EachPage(func(p pagination.Page) (bool, error) {
// Extract the extensions from this page
exts, err := extensions.ExtractExtensions(p)
if err != nil {
return false, err
}
for _, ext := range exts {
log.Printf("[DEBUG] Discovered extension: %s", ext.Alias)
result[ext.Alias] = struct{}{}
}
return true, nil
})
if err != nil {
err = fmt.Errorf("Error loading extensions: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("extensions", result)
return multistep.ActionContinue
}
func (s *StepLoadExtensions) Cleanup(state multistep.StateBag) {
}

7
builder/openstack/step_stop_server.go Normal file → Executable file
View File

@ -15,15 +15,8 @@ type StepStopServer struct{}
func (s *StepStopServer) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(Config)
extensions := state.Get("extensions").(map[string]struct{})
server := state.Get("server").(*servers.Server)
// Verify we have the extension
if _, ok := extensions["os-server-start-stop"]; !ok {
ui.Say("OpenStack cluster doesn't support stop, skipping...")
return multistep.ActionContinue
}
// We need the v2 compute client
client, err := config.computeV2Client()
if err != nil {

View File

@ -0,0 +1,48 @@
package classic
import (
"fmt"
"github.com/hashicorp/go-oracle-terraform/compute"
)
// Artifact is an artifact implementation that contains Image List
// and Machine Image info.
type Artifact struct {
MachineImageName string
MachineImageFile string
ImageListVersion int
driver *compute.ComputeClient
}
// BuilderId uniquely identifies the builder.
func (a *Artifact) BuilderId() string {
return BuilderId
}
// Files lists the files associated with an artifact. We don't have any files
// as the custom image is stored server side.
func (a *Artifact) Files() []string {
return nil
}
func (a *Artifact) Id() string {
return a.MachineImageName
}
func (a *Artifact) String() string {
return fmt.Sprintf("An image list entry was created: \n"+
"Name: %s\n"+
"File: %s\n"+
"Version: %d",
a.MachineImageName, a.MachineImageFile, a.ImageListVersion)
}
func (a *Artifact) State(name string) interface{} {
return nil
}
// Destroy deletes the custom image associated with the artifact.
func (a *Artifact) Destroy() error {
return nil
}

View File

@ -0,0 +1,117 @@
package classic
import (
"fmt"
"log"
"os"
"github.com/hashicorp/go-cleanhttp"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/go-oracle-terraform/opc"
ocommon "github.com/hashicorp/packer/builder/oracle/common"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
// BuilderId uniquely identifies the builder
const BuilderId = "packer.oracle.classic"
// Builder is a builder implementation that creates Oracle OCI custom images.
type Builder struct {
config *Config
runner multistep.Runner
}
func (b *Builder) Prepare(rawConfig ...interface{}) ([]string, error) {
config, err := NewConfig(rawConfig...)
if err != nil {
return nil, err
}
b.config = config
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
loggingEnabled := os.Getenv("PACKER_OCI_CLASSIC_LOGGING") != ""
httpClient := cleanhttp.DefaultClient()
config := &opc.Config{
Username: opc.String(b.config.Username),
Password: opc.String(b.config.Password),
IdentityDomain: opc.String(b.config.IdentityDomain),
APIEndpoint: b.config.apiEndpointURL,
LogLevel: opc.LogDebug,
Logger: &Logger{loggingEnabled},
// Logger: # Leave blank to use the default logger, or provide your own
HTTPClient: httpClient,
}
// Create the Compute Client
client, err := compute.NewComputeClient(config)
if err != nil {
return nil, fmt.Errorf("Error creating OPC Compute Client: %s", err)
}
// Populate the state bag
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("hook", hook)
state.Put("ui", ui)
state.Put("client", client)
// Build the steps
steps := []multistep.Step{
&ocommon.StepKeyPair{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("oci_classic_%s.pem", b.config.PackerBuildName),
PrivateKeyFile: b.config.Comm.SSHPrivateKey,
},
&stepCreateIPReservation{},
&stepAddKeysToAPI{},
&stepSecurity{},
&stepCreateInstance{},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: ocommon.CommHost,
SSHConfig: ocommon.SSHConfig(
b.config.Comm.SSHUsername,
b.config.Comm.SSHPassword),
},
&common.StepProvision{},
&stepSnapshot{},
&stepListImages{},
}
// Run the steps
b.runner = common.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If there is no snapshot, then just return
if _, ok := state.GetOk("snapshot"); !ok {
return nil, nil
}
// Build the artifact and return it
artifact := &Artifact{
ImageListVersion: state.Get("image_list_version").(int),
MachineImageName: state.Get("machine_image_name").(string),
MachineImageFile: state.Get("machine_image_file").(string),
driver: client,
}
return artifact, nil
}
// Cancel terminates a running build.
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}

View File

@ -0,0 +1,137 @@
package classic
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/url"
"os"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
attribs map[string]interface{}
// Access config overrides
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
IdentityDomain string `mapstructure:"identity_domain"`
APIEndpoint string `mapstructure:"api_endpoint"`
apiEndpointURL *url.URL
// Image
ImageName string `mapstructure:"image_name"`
Shape string `mapstructure:"shape"`
SourceImageList string `mapstructure:"source_image_list"`
DestImageList string `mapstructure:"dest_image_list"`
// Attributes and Atributes file are both optional and mutually exclusive.
Attributes string `mapstructure:"attributes"`
AttributesFile string `mapstructure:"attributes_file"`
// Optional; if you don't enter anything, the image list description
// will read "Packer-built image list"
DestImageListDescription string `mapstructure:"image_description"`
// Optional. Describes what computers are allowed to reach your instance
// via SSH. This whitelist must contain the computer you're running Packer
// from. It defaults to public-internet, meaning that you can SSH into your
// instance from anywhere as long as you have the right keys
SSHSourceList string `mapstructure:"ssh_source_list"`
ctx interpolate.Context
}
func NewConfig(raws ...interface{}) (*Config, error) {
c := &Config{}
// Decode from template
err := config.Decode(c, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: &c.ctx,
}, raws...)
if err != nil {
return nil, fmt.Errorf("Failed to mapstructure Config: %+v", err)
}
c.apiEndpointURL, err = url.Parse(c.APIEndpoint)
if err != nil {
return nil, fmt.Errorf("Error parsing API Endpoint: %s", err)
}
// Set default source list
if c.SSHSourceList == "" {
c.SSHSourceList = "seciplist:/oracle/public/public-internet"
}
// Use default oracle username with sudo privileges
if c.Comm.SSHUsername == "" {
c.Comm.SSHUsername = "opc"
}
// Validate that all required fields are present
var errs *packer.MultiError
required := map[string]string{
"username": c.Username,
"password": c.Password,
"api_endpoint": c.APIEndpoint,
"identity_domain": c.IdentityDomain,
"source_image_list": c.SourceImageList,
"dest_image_list": c.DestImageList,
"shape": c.Shape,
}
for k, v := range required {
if v == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("You must specify a %s.", k))
}
}
if c.Attributes != "" && c.AttributesFile != "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.AttributesFile != "" {
if _, err := os.Stat(c.AttributesFile); err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("attributes_file not found: %s", c.AttributesFile))
}
}
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.Comm.Type == "winrm" {
err = fmt.Errorf("winRM is not supported with the oracle-classic builder yet.")
errs = packer.MultiErrorAppend(errs, err)
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
// unpack attributes from json into config
var data map[string]interface{}
if c.Attributes != "" {
err := json.Unmarshal([]byte(c.Attributes), &data)
if err != nil {
err = fmt.Errorf("Problem parsing json from attributes: %s", err)
packer.MultiErrorAppend(errs, err)
}
c.attribs = data
} else if c.AttributesFile != "" {
fidata, err := ioutil.ReadFile(c.AttributesFile)
if err != nil {
err = fmt.Errorf("Problem reading attributes_file: %s", err)
packer.MultiErrorAppend(errs, err)
}
err = json.Unmarshal(fidata, &data)
c.attribs = data
if err != nil {
err = fmt.Errorf("Problem parsing json from attrinutes_file: %s", err)
packer.MultiErrorAppend(errs, err)
}
c.attribs = data
}
return c, nil
}

View File

@ -0,0 +1,61 @@
package classic
import (
"testing"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"identity_domain": "abc12345",
"username": "test@hashicorp.com",
"password": "testpassword123",
"api_endpoint": "https://api-test.compute.test.oraclecloud.com/",
"dest_image_list": "/Config-thing/myuser/myimage",
"source_image_list": "/oracle/public/whatever",
"shape": "oc3",
"image_name": "TestImageName",
"ssh_username": "opc",
}
}
func TestConfigAutoFillsSourceList(t *testing.T) {
tc := testConfig()
conf, err := NewConfig(tc)
if err != nil {
t.Fatalf("Should not have error: %s", err.Error())
}
if conf.SSHSourceList != "seciplist:/oracle/public/public-internet" {
t.Fatalf("conf.SSHSourceList should have been "+
"\"seciplist:/oracle/public/public-internet\" but is \"%s\"",
conf.SSHSourceList)
}
}
func TestConfigValidationCatchesMissing(t *testing.T) {
required := []string{
"username",
"password",
"api_endpoint",
"identity_domain",
"dest_image_list",
"source_image_list",
"shape",
}
for _, key := range required {
tc := testConfig()
delete(tc, key)
_, err := NewConfig(tc)
if err == nil {
t.Fatalf("Test should have failed when config lacked %s!", key)
}
}
}
func TestValidationsIgnoresOptional(t *testing.T) {
tc := testConfig()
delete(tc, "ssh_username")
_, err := NewConfig(tc)
if err != nil {
t.Fatalf("Shouldn't care if ssh_username is missing: err: %#v", err.Error())
}
}

View File

@ -0,0 +1,14 @@
package classic
import "log"
type Logger struct {
Enabled bool
}
func (l *Logger) Log(input ...interface{}) {
if !l.Enabled {
return
}
log.Println(input...)
}

View File

@ -0,0 +1,63 @@
package classic
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepAddKeysToAPI struct{}
func (s *stepAddKeysToAPI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// get variables from state
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*Config)
client := state.Get("client").(*compute.ComputeClient)
// grab packer-generated key from statebag context.
sshPublicKey := strings.TrimSpace(state.Get("publicKey").(string))
// form API call to add key to compute cloud
sshKeyName := fmt.Sprintf("/Compute-%s/%s/packer_generated_key_%s",
config.IdentityDomain, config.Username, uuid.TimeOrderedUUID())
ui.Say(fmt.Sprintf("Creating temporary key: %s", sshKeyName))
sshKeysClient := client.SSHKeys()
sshKeysInput := compute.CreateSSHKeyInput{
Name: sshKeyName,
Key: sshPublicKey,
Enabled: true,
}
// Load the packer-generated SSH key into the Oracle Compute cloud.
keyInfo, err := sshKeysClient.CreateSSHKey(&sshKeysInput)
if err != nil {
err = fmt.Errorf("Problem adding Public SSH key through Oracle's API: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("key_name", keyInfo.Name)
return multistep.ActionContinue
}
func (s *stepAddKeysToAPI) Cleanup(state multistep.StateBag) {
// Delete the keys we created during this run
keyName := state.Get("key_name").(string)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting SSH keys...")
deleteInput := compute.DeleteSSHKeyInput{Name: keyName}
client := state.Get("client").(*compute.ComputeClient)
deleteClient := client.SSHKeys()
err := deleteClient.DeleteSSHKey(&deleteInput)
if err != nil {
ui.Error(fmt.Sprintf("Error deleting SSH keys: %s", err.Error()))
}
return
}

View File

@ -0,0 +1,80 @@
package classic
import (
"context"
"fmt"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepCreateInstance struct{}
func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// get variables from state
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating Instance...")
config := state.Get("config").(*Config)
client := state.Get("client").(*compute.ComputeClient)
keyName := state.Get("key_name").(string)
ipAddName := state.Get("ipres_name").(string)
secListName := state.Get("security_list").(string)
netInfo := compute.NetworkingInfo{
Nat: []string{ipAddName},
SecLists: []string{secListName},
}
// get instances client
instanceClient := client.Instances()
// Instances Input
input := &compute.CreateInstanceInput{
Name: config.ImageName,
Shape: config.Shape,
ImageList: config.SourceImageList,
SSHKeys: []string{keyName},
Networking: map[string]compute.NetworkingInfo{"eth0": netInfo},
Attributes: config.attribs,
}
instanceInfo, err := instanceClient.CreateInstance(input)
if err != nil {
err = fmt.Errorf("Problem creating instance: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("instance_id", instanceInfo.ID)
ui.Message(fmt.Sprintf("Created instance: %s.", instanceInfo.ID))
return multistep.ActionContinue
}
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
// terminate instance
ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(*compute.ComputeClient)
config := state.Get("config").(*Config)
imID := state.Get("instance_id").(string)
ui.Say("Terminating source instance...")
instanceClient := client.Instances()
input := &compute.DeleteInstanceInput{
Name: config.ImageName,
ID: imID,
}
err := instanceClient.DeleteInstance(input)
if err != nil {
err = fmt.Errorf("Problem destroying instance: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return
}
// TODO wait for instance state to change to deleted?
ui.Say("Terminated instance.")
}

View File

@ -0,0 +1,57 @@
package classic
import (
"context"
"fmt"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepCreateIPReservation struct{}
func (s *stepCreateIPReservation) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*Config)
client := state.Get("client").(*compute.ComputeClient)
iprClient := client.IPReservations()
// TODO: add optional Name and Tags
ipresName := fmt.Sprintf("ipres_%s_%s", config.ImageName, uuid.TimeOrderedUUID())
ui.Message(fmt.Sprintf("Creating temporary IP reservation: %s", ipresName))
IPInput := &compute.CreateIPReservationInput{
ParentPool: compute.PublicReservationPool,
Permanent: true,
Name: ipresName,
}
ipRes, err := iprClient.CreateIPReservation(IPInput)
if err != nil {
err := fmt.Errorf("Error creating IP Reservation: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
state.Put("instance_ip", ipRes.IP)
state.Put("ipres_name", ipresName)
return multistep.ActionContinue
}
func (s *stepCreateIPReservation) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
ui.Say("Cleaning up IP reservations...")
client := state.Get("client").(*compute.ComputeClient)
ipResName := state.Get("ipres_name").(string)
input := compute.DeleteIPReservationInput{Name: ipResName}
ipClient := client.IPReservations()
err := ipClient.DeleteIPReservation(&input)
if err != nil {
fmt.Printf("error deleting IP reservation: %s", err.Error())
}
}

View File

@ -0,0 +1,104 @@
package classic
import (
"context"
"fmt"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepListImages struct{}
func (s *stepListImages) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// get variables from state
ui := state.Get("ui").(packer.Ui)
config := state.Get("config").(*Config)
client := state.Get("client").(*compute.ComputeClient)
ui.Say("Adding image to image list...")
imageListClient := client.ImageList()
getInput := compute.GetImageListInput{
Name: config.DestImageList,
}
imList, err := imageListClient.GetImageList(&getInput)
if err != nil {
// If the list didn't exist, create it.
ui.Say(fmt.Sprintf(err.Error()))
ui.Say(fmt.Sprintf("Destination image list %s does not exist; Creating it...",
config.DestImageList))
ilInput := compute.CreateImageListInput{
Name: config.DestImageList,
Description: "Packer-built image list",
}
imList, err = imageListClient.CreateImageList(&ilInput)
if err != nil {
err = fmt.Errorf("Problem creating image list: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
ui.Message(fmt.Sprintf("Image list %s created!", imList.URI))
}
// Now create and image list entry for the image into that list.
snap := state.Get("snapshot").(*compute.Snapshot)
version := len(imList.Entries) + 1
entriesClient := client.ImageListEntries()
entriesInput := compute.CreateImageListEntryInput{
Name: config.DestImageList,
MachineImages: []string{fmt.Sprintf("/Compute-%s/%s/%s",
config.IdentityDomain, config.Username, snap.MachineImage)},
Version: version,
}
entryInfo, err := entriesClient.CreateImageListEntry(&entriesInput)
if err != nil {
err = fmt.Errorf("Problem creating an image list entry: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("image_list_entry", entryInfo)
ui.Message(fmt.Sprintf("created image list entry %s", entryInfo.Name))
machineImagesClient := client.MachineImages()
getImagesInput := compute.GetMachineImageInput{
Name: config.ImageName,
}
// Update image list default to use latest version
updateInput := compute.UpdateImageListInput{
Default: version,
Description: config.DestImageListDescription,
Name: config.DestImageList,
}
_, err = imageListClient.UpdateImageList(&updateInput)
if err != nil {
err = fmt.Errorf("Problem updating default image list version: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
// Grab info about the machine image to return with the artifact
imInfo, err := machineImagesClient.GetMachineImage(&getImagesInput)
if err != nil {
err = fmt.Errorf("Problem getting machine image info: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("machine_image_file", imInfo.File)
state.Put("machine_image_name", imInfo.Name)
state.Put("image_list_version", version)
return multistep.ActionContinue
}
func (s *stepListImages) Cleanup(state multistep.StateBag) {
// Nothing to do
return
}

View File

@ -0,0 +1,95 @@
package classic
import (
"context"
"fmt"
"log"
"strings"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepSecurity struct{}
func (s *stepSecurity) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
ui.Say("Configuring security lists and rules to enable SSH access...")
config := state.Get("config").(*Config)
client := state.Get("client").(*compute.ComputeClient)
secListName := fmt.Sprintf("/Compute-%s/%s/Packer_SSH_Allow_%s",
config.IdentityDomain, config.Username, config.ImageName)
secListClient := client.SecurityLists()
secListInput := compute.CreateSecurityListInput{
Description: "Packer-generated security list to give packer ssh access",
Name: secListName,
}
_, err := secListClient.CreateSecurityList(&secListInput)
if err != nil {
if !strings.Contains(err.Error(), "already exists") {
err = fmt.Errorf("Error creating security List to"+
" allow Packer to connect to Oracle instance via SSH: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
}
// DOCS NOTE: user must have Compute_Operations role
// Create security rule that allows Packer to connect via SSH
secRulesClient := client.SecRules()
secRulesInput := compute.CreateSecRuleInput{
Action: "PERMIT",
Application: "/oracle/public/ssh",
Description: "Packer-generated security rule to allow ssh",
DestinationList: fmt.Sprintf("seclist:%s", secListName),
Name: fmt.Sprintf("Packer-allow-SSH-Rule_%s", config.ImageName),
SourceList: config.SSHSourceList,
}
secRuleName := fmt.Sprintf("/Compute-%s/%s/Packer-allow-SSH-Rule_%s",
config.IdentityDomain, config.Username, config.ImageName)
_, err = secRulesClient.CreateSecRule(&secRulesInput)
if err != nil {
log.Printf("Error creating security rule to allow SSH: %s", err.Error())
if !strings.Contains(err.Error(), "already exists") {
err = fmt.Errorf("Error creating security rule to"+
" allow Packer to connect to Oracle instance via SSH: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
}
state.Put("security_rule_name", secRuleName)
state.Put("security_list", secListName)
return multistep.ActionContinue
}
func (s *stepSecurity) Cleanup(state multistep.StateBag) {
client := state.Get("client").(*compute.ComputeClient)
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting temporary rules and lists...")
// delete security rules that Packer generated
secRuleName := state.Get("security_rule_name").(string)
secRulesClient := client.SecRules()
ruleInput := compute.DeleteSecRuleInput{Name: secRuleName}
err := secRulesClient.DeleteSecRule(&ruleInput)
if err != nil {
ui.Say(fmt.Sprintf("Error deleting the packer-generated security rule %s; "+
"please delete manually. (error: %s)", secRuleName, err.Error()))
}
// delete security list that Packer generated
secListName := state.Get("security_list").(string)
secListClient := client.SecurityLists()
input := compute.DeleteSecurityListInput{Name: secListName}
err = secListClient.DeleteSecurityList(&input)
if err != nil {
ui.Say(fmt.Sprintf("Error deleting the packer-generated security list %s; "+
"please delete manually. (error : %s)", secListName, err.Error()))
}
}

View File

@ -0,0 +1,63 @@
package classic
import (
"context"
"fmt"
"github.com/hashicorp/go-oracle-terraform/compute"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type stepSnapshot struct{}
func (s *stepSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// get variables from state
ui := state.Get("ui").(packer.Ui)
ui.Say("Creating Snapshot...")
config := state.Get("config").(*Config)
client := state.Get("client").(*compute.ComputeClient)
instanceID := state.Get("instance_id").(string)
// get instances client
snapshotClient := client.Snapshots()
// Instances Input
snapshotInput := &compute.CreateSnapshotInput{
Instance: fmt.Sprintf("%s/%s", config.ImageName, instanceID),
MachineImage: config.ImageName,
}
snap, err := snapshotClient.CreateSnapshot(snapshotInput)
if err != nil {
err = fmt.Errorf("Problem creating snapshot: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return multistep.ActionHalt
}
state.Put("snapshot", snap)
ui.Message(fmt.Sprintf("Created snapshot: %s.", snap.Name))
return multistep.ActionContinue
}
func (s *stepSnapshot) Cleanup(state multistep.StateBag) {
// Delete the snapshot
ui := state.Get("ui").(packer.Ui)
ui.Say("Deleting Snapshot...")
client := state.Get("client").(*compute.ComputeClient)
snap := state.Get("snapshot").(*compute.Snapshot)
snapClient := client.Snapshots()
snapInput := compute.DeleteSnapshotInput{
Snapshot: snap.Name,
MachineImage: snap.MachineImage,
}
err := snapClient.DeleteSnapshotResourceOnly(&snapInput)
if err != nil {
err = fmt.Errorf("Problem deleting snapshot: %s", err)
ui.Error(err.Error())
state.Put("error", err)
}
return
}

View File

@ -1,4 +1,4 @@
package oci
package common
import (
"fmt"
@ -8,7 +8,7 @@ import (
"golang.org/x/crypto/ssh"
)
func commHost(state multistep.StateBag) (string, error) {
func CommHost(state multistep.StateBag) (string, error) {
ipAddress := state.Get("instance_ip").(string)
return ipAddress, nil
}

View File

@ -1,4 +1,4 @@
package oci
package common
import (
"context"
@ -16,13 +16,13 @@ import (
"golang.org/x/crypto/ssh"
)
type stepKeyPair struct {
type StepKeyPair struct {
Debug bool
DebugKeyPath string
PrivateKeyFile string
}
func (s *stepKeyPair) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepKeyPair) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if s.PrivateKeyFile != "" {
@ -112,6 +112,6 @@ func (s *stepKeyPair) Run(_ context.Context, state multistep.StateBag) multistep
return multistep.ActionContinue
}
func (s *stepKeyPair) Cleanup(state multistep.StateBag) {
func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
// Nothing to do
}

View File

@ -6,6 +6,7 @@ import (
"fmt"
"log"
ocommon "github.com/hashicorp/packer/builder/oracle/common"
client "github.com/hashicorp/packer/builder/oracle/oci/client"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
@ -50,7 +51,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Build the steps
steps := []multistep.Step{
&stepKeyPair{
&ocommon.StepKeyPair{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("oci_%s.pem", b.config.PackerBuildName),
PrivateKeyFile: b.config.Comm.SSHPrivateKey,
@ -59,8 +60,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&stepInstanceInfo{},
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: SSHConfig(
Host: ocommon.CommHost,
SSHConfig: ocommon.SSHConfig(
b.config.Comm.SSHUsername,
b.config.Comm.SSHPassword),
},

View File

@ -25,6 +25,7 @@ var accels = map[string]struct{}{
"kvm": {},
"tcg": {},
"xen": {},
"hax": {},
}
var netDevice = map[string]bool{
@ -259,7 +260,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
if _, ok := accels[b.config.Accelerator]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', or 'none' are allowed"))
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', or 'none' are allowed"))
}
if _, ok := netDevice[b.config.NetDevice]; !ok {

View File

@ -0,0 +1,62 @@
package scaleway
import (
"fmt"
"log"
"github.com/scaleway/scaleway-cli/pkg/api"
)
type Artifact struct {
// The name of the image
imageName string
// The ID of the image
imageID string
// The name of the snapshot
snapshotName string
// The ID of the snapshot
snapshotID string
// The name of the region
regionName string
// The client for making API calls
client *api.ScalewayAPI
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (*Artifact) Files() []string {
// No files with Scaleway
return nil
}
func (a *Artifact) Id() string {
return fmt.Sprintf("%s:%s", a.regionName, 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)
}
func (a *Artifact) State(name string) interface{} {
return nil
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %s (%s)", a.imageID, a.imageName)
if err := a.client.DeleteImage(a.imageID); err != nil {
return err
}
log.Printf("Destroying snapshot: %s (%s)", a.snapshotID, a.snapshotName)
if err := a.client.DeleteSnapshot(a.snapshotID); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,33 @@
package scaleway
import (
"testing"
"github.com/hashicorp/packer/packer"
)
func TestArtifact_Impl(t *testing.T) {
var raw interface{}
raw = &Artifact{}
if _, ok := raw.(packer.Artifact); !ok {
t.Fatalf("Artifact should be artifact")
}
}
func TestArtifactId(t *testing.T) {
a := &Artifact{"packer-foobar-image", "cc586e45-5156-4f71-b223-cf406b10dd1d", "packer-foobar-snapshot", "cc586e45-5156-4f71-b223-cf406b10dd1c", "ams1", nil}
expected := "ams1:cc586e45-5156-4f71-b223-cf406b10dd1d"
if a.Id() != expected {
t.Fatalf("artifact ID should match: %v", expected)
}
}
func TestArtifactString(t *testing.T) {
a := &Artifact{"packer-foobar-image", "cc586e45-5156-4f71-b223-cf406b10dd1d", "packer-foobar-snapshot", "cc586e45-5156-4f71-b223-cf406b10dd1c", "ams1", nil}
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)"
if a.String() != expected {
t.Fatalf("artifact string should match: %v", expected)
}
}

106
builder/scaleway/builder.go Normal file
View File

@ -0,0 +1,106 @@
// The scaleway package contains a packer.Builder implementation
// that builds Scaleway images (snapshots).
package scaleway
import (
"errors"
"fmt"
"log"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
)
// The unique id for the builder
const BuilderId = "hashicorp.scaleway"
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
c, warnings, errs := NewConfig(raws...)
if errs != nil {
return warnings, errs
}
b.config = *c
return nil, nil
}
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
client, err := api.NewScalewayAPI(b.config.Organization, b.config.Token, b.config.UserAgent, b.config.Region)
if err != nil {
ui.Error(err.Error())
return nil, err
}
state := new(multistep.BasicStateBag)
state.Put("config", b.config)
state.Put("client", client)
state.Put("hook", hook)
state.Put("ui", ui)
steps := []multistep.Step{
&stepCreateSSHKey{
Debug: b.config.PackerDebug,
DebugKeyPath: fmt.Sprintf("scw_%s.pem", b.config.PackerBuildName),
PrivateKeyFile: b.config.Comm.SSHPrivateKey,
},
new(stepCreateServer),
new(stepServerInfo),
&communicator.StepConnect{
Config: &b.config.Comm,
Host: commHost,
SSHConfig: sshConfig,
},
new(common.StepProvision),
new(stepShutdown),
new(stepSnapshot),
new(stepImage),
}
b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
b.runner.Run(state)
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
// If we were interrupted or cancelled, then just exit.
if _, ok := state.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("Build was cancelled.")
}
if _, ok := state.GetOk(multistep.StateHalted); ok {
return nil, errors.New("Build was halted.")
}
if _, ok := state.GetOk("snapshot_name"); !ok {
return nil, errors.New("Cannot find snapshot_name in state.")
}
artifact := &Artifact{
imageName: state.Get("image_name").(string),
imageID: state.Get("image_id").(string),
snapshotName: state.Get("snapshot_name").(string),
snapshotID: state.Get("snapshot_id").(string),
regionName: state.Get("region").(string),
client: client,
}
return artifact, nil
}
func (b *Builder) Cancel() {
if b.runner != nil {
log.Println("Cancelling the step runner...")
b.runner.Cancel()
}
}

View File

@ -0,0 +1,237 @@
package scaleway
import (
"strconv"
"testing"
"github.com/hashicorp/packer/packer"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"api_access_key": "foo",
"api_token": "bar",
"region": "ams1",
"commercial_type": "VC1S",
"ssh_username": "root",
"image": "image-uuid",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packer.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilder_Prepare_BadType(t *testing.T) {
b := &Builder{}
c := map[string]interface{}{
"api_token": []string{},
}
warnings, err := b.Prepare(c)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
config["i_should_not_be_valid"] = true
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "region")
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should error")
}
expected := "ams1"
config["region"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != expected {
t.Errorf("found %s, expected %s", b.config.Region, expected)
}
}
func TestBuilderPrepare_CommercialType(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "commercial_type")
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should error")
}
expected := "VC1S"
config["commercial_type"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.CommercialType != expected {
t.Errorf("found %s, expected %s", b.config.CommercialType, expected)
}
}
func TestBuilderPrepare_Image(t *testing.T) {
var b Builder
config := testConfig()
delete(config, "image")
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should error")
}
expected := "cc586e45-5156-4f71-b223-cf406b10dd1c"
config["image"] = expected
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Image != expected {
t.Errorf("found %s, expected %s", b.config.Image, expected)
}
}
func TestBuilderPrepare_SnapshotName(t *testing.T) {
var b Builder
config := testConfig()
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SnapshotName == "" {
t.Errorf("invalid: %s", b.config.SnapshotName)
}
config["snapshot_name"] = "foobarbaz"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["snapshot_name"] = "{{timestamp}}"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
_, err = strconv.ParseInt(b.config.SnapshotName, 0, 0)
if err != nil {
t.Fatalf("failed to parse int in template: %s", err)
}
}
func TestBuilderPrepare_ServerName(t *testing.T) {
var b Builder
config := testConfig()
warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.ServerName == "" {
t.Errorf("invalid: %s", b.config.ServerName)
}
config["server_name"] = "foobar"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["server_name"] = "foobar-{{timestamp}}"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
config["server_name"] = "foobar-{{"
b = Builder{}
warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}

122
builder/scaleway/config.go Normal file
View File

@ -0,0 +1,122 @@
package scaleway
import (
"errors"
"fmt"
"os"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/common/uuid"
"github.com/hashicorp/packer/helper/communicator"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/template/interpolate"
"github.com/mitchellh/mapstructure"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
Token string `mapstructure:"api_token"`
Organization string `mapstructure:"api_access_key"`
Region string `mapstructure:"region"`
Image string `mapstructure:"image"`
CommercialType string `mapstructure:"commercial_type"`
SnapshotName string `mapstructure:"snapshot_name"`
ImageName string `mapstructure:"image_name"`
ServerName string `mapstructure:"server_name"`
UserAgent string
ctx interpolate.Context
}
func NewConfig(raws ...interface{}) (*Config, []string, error) {
c := new(Config)
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
}, raws...)
if err != nil {
return nil, nil, err
}
c.UserAgent = "Packer - Scaleway builder"
if c.Organization == "" {
c.Organization = os.Getenv("SCALEWAY_API_ACCESS_KEY")
}
if c.Token == "" {
c.Token = os.Getenv("SCALEWAY_API_TOKEN")
}
if c.SnapshotName == "" {
def, err := interpolate.Render("snapshot-packer-{{timestamp}}", nil)
if err != nil {
panic(err)
}
c.SnapshotName = def
}
if c.ImageName == "" {
def, err := interpolate.Render("image-packer-{{timestamp}}", nil)
if err != nil {
panic(err)
}
c.ImageName = def
}
if c.ServerName == "" {
// Default to packer-[time-ordered-uuid]
c.ServerName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
var errs *packer.MultiError
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packer.MultiErrorAppend(errs, es...)
}
if c.Organization == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Scaleway Organization ID must be specified"))
}
if c.Token == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("Scaleway Token must be specified"))
}
if c.Region == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("region is required"))
}
if c.CommercialType == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("commercial type is required"))
}
if c.Image == "" {
errs = packer.MultiErrorAppend(
errs, errors.New("image is required"))
}
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}
common.ScrubConfig(c, c.Token)
return c, nil, nil
}

64
builder/scaleway/ssh.go Normal file
View File

@ -0,0 +1,64 @@
package scaleway
import (
"fmt"
"net"
"os"
packerssh "github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/multistep"
"golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
func commHost(state multistep.StateBag) (string, error) {
ipAddress := state.Get("server_ip").(string)
return ipAddress, nil
}
func sshConfig(state multistep.StateBag) (*ssh.ClientConfig, error) {
config := state.Get("config").(Config)
var privateKey string
var auth []ssh.AuthMethod
if config.Comm.SSHAgentAuth {
authSock := os.Getenv("SSH_AUTH_SOCK")
if authSock == "" {
return nil, fmt.Errorf("SSH_AUTH_SOCK is not set")
}
sshAgent, err := net.Dial("unix", authSock)
if err != nil {
return nil, fmt.Errorf("Cannot connect to SSH Agent socket %q: %s", authSock, err)
}
auth = []ssh.AuthMethod{
ssh.PublicKeysCallback(agent.NewClient(sshAgent).Signers),
}
}
if config.Comm.SSHPassword != "" {
auth = append(auth,
ssh.Password(config.Comm.SSHPassword),
ssh.KeyboardInteractive(
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
)
}
if config.Comm.SSHPrivateKey != "" {
if priv, ok := state.GetOk("privateKey"); ok {
privateKey = priv.(string)
}
signer, err := ssh.ParsePrivateKey([]byte(privateKey))
if err != nil {
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
}
auth = append(auth, ssh.PublicKeys(signer))
}
return &ssh.ClientConfig{
User: config.Comm.SSHUsername,
Auth: auth,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
}, nil
}

View File

@ -0,0 +1,54 @@
package scaleway
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
)
type stepImage struct{}
func (s *stepImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
snapshotID := state.Get("snapshot_id").(string)
bootscriptID := ""
ui.Say(fmt.Sprintf("Creating image: %v", c.ImageName))
image, err := client.GetImage(c.Image)
if err != nil {
err := fmt.Errorf("Error getting initial image info: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if image.DefaultBootscript != nil {
bootscriptID = image.DefaultBootscript.Identifier
}
imageID, err := client.PostImage(snapshotID, c.ImageName, bootscriptID, image.Arch)
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Image ID: %s", imageID)
state.Put("image_id", imageID)
state.Put("image_name", c.ImageName)
state.Put("region", c.Region)
return multistep.ActionContinue
}
func (s *stepImage) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -0,0 +1,78 @@
package scaleway
import (
"context"
"fmt"
"strings"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
)
type stepCreateServer struct {
serverID string
}
func (s *stepCreateServer) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
ui := state.Get("ui").(packer.Ui)
c := state.Get("config").(Config)
sshPubKey := state.Get("ssh_pubkey").(string)
tags := []string{}
ui.Say("Creating server...")
if sshPubKey != "" {
tags = []string{fmt.Sprintf("AUTHORIZED_KEY=%s", strings.TrimSpace(sshPubKey))}
}
server, err := client.PostServer(api.ScalewayServerDefinition{
Name: c.ServerName,
Image: &c.Image,
Organization: c.Organization,
CommercialType: c.CommercialType,
Tags: tags,
})
if err != nil {
err := fmt.Errorf("Error creating server: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
err = client.PostServerAction(server, "poweron")
if err != nil {
err := fmt.Errorf("Error starting server: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
s.serverID = server
state.Put("server_id", server)
return multistep.ActionContinue
}
func (s *stepCreateServer) Cleanup(state multistep.StateBag) {
if s.serverID == "" {
return
}
client := state.Get("client").(*api.ScalewayAPI)
ui := state.Get("ui").(packer.Ui)
ui.Say("Destroying server...")
err := client.DeleteServerForce(s.serverID)
if err != nil {
ui.Error(fmt.Sprintf(
"Error destroying server. Please destroy it manually: %s", err))
}
}

View File

@ -0,0 +1,106 @@
package scaleway
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"fmt"
"io/ioutil"
"log"
"os"
"runtime"
"strings"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"golang.org/x/crypto/ssh"
)
type stepCreateSSHKey struct {
Debug bool
DebugKeyPath string
PrivateKeyFile string
}
func (s *stepCreateSSHKey) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
if s.PrivateKeyFile != "" {
ui.Say("Using existing SSH private key")
privateKeyBytes, err := ioutil.ReadFile(s.PrivateKeyFile)
if err != nil {
state.Put("error", fmt.Errorf(
"Error loading configured private key file: %s", err))
return multistep.ActionHalt
}
state.Put("privateKey", string(privateKeyBytes))
state.Put("ssh_pubkey", "")
return multistep.ActionContinue
}
ui.Say("Creating temporary ssh key for server...")
priv, err := rsa.GenerateKey(rand.Reader, 2014)
if err != nil {
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// ASN.1 DER encoded form
priv_der := x509.MarshalPKCS1PrivateKey(priv)
priv_blk := pem.Block{
Type: "RSA PRIVATE KEY",
Headers: nil,
Bytes: priv_der,
}
// Set the private key in the statebag for later
state.Put("privateKey", string(pem.EncodeToMemory(&priv_blk)))
pub, _ := ssh.NewPublicKey(&priv.PublicKey)
pub_sshformat := string(ssh.MarshalAuthorizedKey(pub))
pub_sshformat = strings.Replace(pub_sshformat, " ", "_", -1)
log.Printf("temporary ssh key created")
// Remember some state for the future
state.Put("ssh_pubkey", string(pub_sshformat))
// If we're in debug mode, output the private key to the working directory.
if s.Debug {
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
f, err := os.Create(s.DebugKeyPath)
if err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
defer f.Close()
// Write the key out
if _, err := f.Write(pem.EncodeToMemory(&priv_blk)); err != nil {
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
return multistep.ActionHalt
}
// Chmod it so that it is SSH ready
if runtime.GOOS != "windows" {
if err := f.Chmod(0600); err != nil {
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
return multistep.ActionHalt
}
}
}
return multistep.ActionContinue
}
func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
// SSH key is passed via tag. Nothing to do here.
return
}

View File

@ -0,0 +1,45 @@
package scaleway
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
)
type stepServerInfo struct{}
func (s *stepServerInfo) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
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")
if err != nil {
err := fmt.Errorf("Error waiting for server to become booted: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
server, err := client.GetServer(serverID)
if err != nil {
err := fmt.Errorf("Error retrieving server: %s", err)
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)
return multistep.ActionContinue
}
func (s *stepServerInfo) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -0,0 +1,44 @@
package scaleway
import (
"context"
"fmt"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
)
type stepShutdown struct{}
func (s *stepShutdown) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
ui := state.Get("ui").(packer.Ui)
serverID := state.Get("server_id").(string)
ui.Say("Shutting down server...")
err := client.PostServerAction(serverID, "poweroff")
if err != nil {
err := fmt.Errorf("Error stopping server: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
_, err = api.WaitForServerState(client, serverID, "stopped")
if err != nil {
err := fmt.Errorf("Error shutting down server: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -0,0 +1,40 @@
package scaleway
import (
"context"
"fmt"
"log"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
"github.com/scaleway/scaleway-cli/pkg/api"
)
type stepSnapshot struct{}
func (s *stepSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*api.ScalewayAPI)
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)
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Snapshot ID: %s", snapshot)
state.Put("snapshot_id", snapshot)
state.Put("snapshot_name", c.SnapshotName)
state.Put("region", c.Region)
return multistep.ActionContinue
}
func (s *stepSnapshot) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -122,7 +122,7 @@ func (s *StepDownloadGuestAdditions) Run(ctx context.Context, state multistep.St
}
// Convert the file/url to an actual URL for step_download to process.
url, err = common.DownloadableURL(url)
url, err = common.ValidatedURL(url)
if err != nil {
err := fmt.Errorf("Error preparing guest additions url: %s", err)
state.Put("error", err)

View File

@ -97,7 +97,7 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
if c.SourcePath == "" {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required"))
} else {
c.SourcePath, err = common.DownloadableURL(c.SourcePath)
c.SourcePath, err = common.ValidatedURL(c.SourcePath)
if err != nil {
errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is invalid: %s", err))
}

View File

@ -84,7 +84,7 @@ func TestNewConfig_sourcePath(t *testing.T) {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should error")
t.Fatalf("should error")
}
// Good

View File

@ -2,13 +2,18 @@ package common
import (
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"net"
"os"
"os/exec"
"regexp"
"runtime"
"strconv"
"strings"
"time"
"github.com/hashicorp/packer/helper/multistep"
)
@ -24,15 +29,11 @@ type Driver interface {
CompactDisk(string) error
// CreateDisk creates a virtual disk with the given size.
CreateDisk(string, string, string) error
CreateDisk(string, string, string, string) error
// Checks if the VMX file at the given path is running.
IsRunning(string) (bool, error)
// CommHost returns the host address for the VM that is being
// managed by this driver.
CommHost(multistep.StateBag) (string, error)
// Start starts a VM specified by the path to the VMX given.
Start(string, bool) error
@ -49,14 +50,33 @@ type Driver interface {
// Attach the VMware tools ISO
ToolsInstall() error
// Get the path to the DHCP leases file for the given device.
DhcpLeasesPath(string) string
// Verify checks to make sure that this driver should function
// properly. This should check that all the files it will use
// appear to exist and so on. If everything is okay, this doesn't
// return an error. Otherwise, this returns an error.
// return an error. Otherwise, this returns an error. Each vmware
// driver should assign the VmwareMachine callback functions for locating
// paths within this function.
Verify() error
/// This is to establish a connection to the guest
CommHost(multistep.StateBag) (string, error)
/// These methods are generally implemented by the VmwareDriver
/// structure within this file. A driver implementation can
/// reimplement these, though, if it wants.
GetVmwareDriver() VmwareDriver
// Get the guest hw address for the vm
GuestAddress(multistep.StateBag) (string, error)
// Get the guest ip address for the vm
GuestIP(multistep.StateBag) (string, error)
// Get the host hw address for the vm
HostAddress(multistep.StateBag) (string, error)
// Get the host ip address for the vm
HostIP(multistep.StateBag) (string, error)
}
// NewDriver returns a new driver implementation for this operating
@ -192,3 +212,305 @@ func compareVersions(versionFound string, versionWanted string, product string)
return nil
}
/// helper functions that read configuration information from a file
// read the network<->device configuration out of the specified path
func ReadNetmapConfig(path string) (NetworkMap, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
}
defer fd.Close()
return ReadNetworkMap(fd)
}
// read the dhcp configuration out of the specified path
func ReadDhcpConfig(path string) (DhcpConfiguration, error) {
fd, err := os.Open(path)
if err != nil {
return nil, err
}
defer fd.Close()
return ReadDhcpConfiguration(fd)
}
// read the VMX configuration from the specified path
func readVMXConfig(path string) (map[string]string, error) {
f, err := os.Open(path)
if err != nil {
return map[string]string{}, err
}
defer f.Close()
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
return map[string]string{}, err
}
return ParseVMX(string(vmxBytes)), nil
}
// read the connection type out of a vmx configuration
func readCustomDeviceName(vmxData map[string]string) (string, error) {
connectionType, ok := vmxData["ethernet0.connectiontype"]
if !ok || connectionType != "custom" {
return "", fmt.Errorf("Unable to determine the device name for the connection type : %s", connectionType)
}
device, ok := vmxData["ethernet0.vnet"]
if !ok || device == "" {
return "", fmt.Errorf("Unable to determine the device name for the connection type \"%s\" : %s", connectionType, device)
}
return device, nil
}
// This VmwareDriver is a base class that contains default methods
// that a Driver can use or implement themselves.
type VmwareDriver struct {
/// These methods define paths that are utilized by the driver
/// A driver must overload these in order to point to the correct
/// files so that the address detection (ip and ethernet) machinery
/// works.
DhcpLeasesPath func(string) string
DhcpConfPath func(string) string
VmnetnatConfPath func(string) string
/// This method returns an object with the NetworkNameMapper interface
/// that maps network to device and vice-versa.
NetworkMapper func() (NetworkNameMapper, error)
}
func (d *VmwareDriver) GuestAddress(state multistep.StateBag) (string, error) {
vmxPath := state.Get("vmx_path").(string)
log.Println("Lookup up IP information...")
vmxData, err := readVMXConfig(vmxPath)
if err != nil {
return "", err
}
var ok bool
macAddress := ""
if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" {
if macAddress, ok = vmxData["ethernet0.generatedaddress"]; !ok || macAddress == "" {
return "", errors.New("couldn't find MAC address in VMX")
}
}
res, err := net.ParseMAC(macAddress)
if err != nil {
return "", err
}
return res.String(), nil
}
func (d *VmwareDriver) GuestIP(state multistep.StateBag) (string, error) {
// grab network mapper
netmap, err := d.NetworkMapper()
if err != nil {
return "", err
}
// convert the stashed network to a device
network := state.Get("vmnetwork").(string)
device, err := netmap.NameIntoDevice(network)
// we were unable to find the device, maybe it's a custom one...
// so, check to see if it's in the .vmx configuration
if err != nil || network == "custom" {
vmxPath := state.Get("vmx_path").(string)
vmxData, err := readVMXConfig(vmxPath)
if err != nil {
return "", err
}
device, err = readCustomDeviceName(vmxData)
if err != nil {
return "", err
}
}
// figure out our MAC address for looking up the guest address
MACAddress, err := d.GuestAddress(state)
if err != nil {
return "", err
}
// figure out the correct dhcp leases
dhcpLeasesPath := d.DhcpLeasesPath(device)
log.Printf("DHCP leases path: %s", dhcpLeasesPath)
if dhcpLeasesPath == "" {
return "", errors.New("no DHCP leases path found.")
}
// open up the lease and read its contents
fh, err := os.Open(dhcpLeasesPath)
if err != nil {
return "", err
}
defer fh.Close()
dhcpBytes, err := ioutil.ReadAll(fh)
if err != nil {
return "", err
}
// start grepping through the file looking for fields that we care about
var lastIp string
var lastLeaseEnd time.Time
var curIp string
var curLeaseEnd time.Time
ipLineRe := regexp.MustCompile(`^lease (.+?) {$`)
endTimeLineRe := regexp.MustCompile(`^\s*ends \d (.+?);$`)
macLineRe := regexp.MustCompile(`^\s*hardware ethernet (.+?);$`)
for _, line := range strings.Split(string(dhcpBytes), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
matches := ipLineRe.FindStringSubmatch(line)
if matches != nil {
lastIp = matches[1]
continue
}
matches = endTimeLineRe.FindStringSubmatch(line)
if matches != nil {
lastLeaseEnd, _ = time.Parse("2006/01/02 15:04:05", matches[1])
continue
}
// If the mac address matches and this lease ends farther in the
// future than the last match we might have, then choose it.
matches = macLineRe.FindStringSubmatch(line)
if matches != nil && strings.EqualFold(matches[1], MACAddress) && curLeaseEnd.Before(lastLeaseEnd) {
curIp = lastIp
curLeaseEnd = lastLeaseEnd
}
}
if curIp == "" {
return "", fmt.Errorf("IP not found for MAC %s in DHCP leases at %s", MACAddress, dhcpLeasesPath)
}
return curIp, nil
}
func (d *VmwareDriver) HostAddress(state multistep.StateBag) (string, error) {
// grab mapper for converting network<->device
netmap, err := d.NetworkMapper()
if err != nil {
return "", err
}
// convert network to name
network := state.Get("vmnetwork").(string)
device, err := netmap.NameIntoDevice(network)
// we were unable to find the device, maybe it's a custom one...
// so, check to see if it's in the .vmx configuration
if err != nil || network == "custom" {
vmxPath := state.Get("vmx_path").(string)
vmxData, err := readVMXConfig(vmxPath)
if err != nil {
return "", err
}
device, err = readCustomDeviceName(vmxData)
if err != nil {
return "", err
}
}
// parse dhcpd configuration
pathDhcpConfig := d.DhcpConfPath(device)
if _, err := os.Stat(pathDhcpConfig); err != nil {
return "", fmt.Errorf("Could not find vmnetdhcp conf file: %s", pathDhcpConfig)
}
config, err := ReadDhcpConfig(pathDhcpConfig)
if err != nil {
return "", err
}
// find the entry configured in the dhcpd
interfaceConfig, err := config.HostByName(device)
if err != nil {
return "", err
}
// finally grab the hardware address
address, err := interfaceConfig.Hardware()
if err == nil {
return address.String(), nil
}
// we didn't find it, so search through our interfaces for the device name
interfaceList, err := net.Interfaces()
if err == nil {
return "", err
}
names := make([]string, 0)
for _, intf := range interfaceList {
if strings.HasSuffix(strings.ToLower(intf.Name), device) {
return intf.HardwareAddr.String(), nil
}
names = append(names, intf.Name)
}
return "", fmt.Errorf("Unable to find device %s : %v", device, names)
}
func (d *VmwareDriver) HostIP(state multistep.StateBag) (string, error) {
// grab mapper for converting network<->device
netmap, err := d.NetworkMapper()
if err != nil {
return "", err
}
// convert network to name
network := state.Get("vmnetwork").(string)
device, err := netmap.NameIntoDevice(network)
// we were unable to find the device, maybe it's a custom one...
// so, check to see if it's in the .vmx configuration
if err != nil || network == "custom" {
vmxPath := state.Get("vmx_path").(string)
vmxData, err := readVMXConfig(vmxPath)
if err != nil {
return "", err
}
device, err = readCustomDeviceName(vmxData)
if err != nil {
return "", err
}
}
// parse dhcpd configuration
pathDhcpConfig := d.DhcpConfPath(device)
if _, err := os.Stat(pathDhcpConfig); err != nil {
return "", fmt.Errorf("Could not find vmnetdhcp conf file: %s", pathDhcpConfig)
}
config, err := ReadDhcpConfig(pathDhcpConfig)
if err != nil {
return "", err
}
// find the entry configured in the dhcpd
interfaceConfig, err := config.HostByName(device)
if err != nil {
return "", err
}
address, err := interfaceConfig.IP4()
if err != nil {
return "", err
}
return address.String(), nil
}

View File

@ -14,6 +14,8 @@ import (
// Fusion5Driver is a driver that can run VMware Fusion 5.
type Fusion5Driver struct {
VmwareDriver
// This is the path to the "VMware Fusion.app"
AppPath string
@ -39,8 +41,8 @@ func (d *Fusion5Driver) CompactDisk(diskPath string) error {
return nil
}
func (d *Fusion5Driver) CreateDisk(output string, size string, type_id string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
func (d *Fusion5Driver) CreateDisk(output string, size string, adapter_type string, type_id string) error {
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", adapter_type, "-t", type_id, output)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
@ -139,6 +141,32 @@ func (d *Fusion5Driver) Verify() error {
return err
}
libpath := filepath.Join("/", "Library", "Preferences", "VMware Fusion")
d.VmwareDriver.DhcpLeasesPath = func(device string) string {
return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases"
}
d.VmwareDriver.DhcpConfPath = func(device string) string {
return filepath.Join(libpath, device, "dhcpd.conf")
}
d.VmwareDriver.VmnetnatConfPath = func(device string) string {
return filepath.Join(libpath, device, "nat.conf")
}
d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) {
pathNetworking := filepath.Join(libpath, "networking")
if _, err := os.Stat(pathNetworking); err != nil {
return nil, fmt.Errorf("Could not find networking conf file: %s", pathNetworking)
}
fd, err := os.Open(pathNetworking)
if err != nil {
return nil, err
}
defer fd.Close()
return ReadNetworkingConfig(fd)
}
return nil
}
@ -158,10 +186,6 @@ func (d *Fusion5Driver) ToolsInstall() error {
return nil
}
func (d *Fusion5Driver) DhcpLeasesPath(device string) string {
return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases"
}
const fusionSuppressPlist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
@ -170,3 +194,7 @@ const fusionSuppressPlist = `<?xml version="1.0" encoding="UTF-8"?>
<true/>
</dict>
</plist>`
func (d *Fusion5Driver) GetVmwareDriver() VmwareDriver {
return d.VmwareDriver
}

View File

@ -66,5 +66,36 @@ func (d *Fusion6Driver) Verify() error {
}
log.Printf("Detected VMware version: %s", matches[1])
libpath := filepath.Join("/", "Library", "Preferences", "VMware Fusion")
d.VmwareDriver.DhcpLeasesPath = func(device string) string {
return "/var/db/vmware/vmnet-dhcpd-" + device + ".leases"
}
d.VmwareDriver.DhcpConfPath = func(device string) string {
return filepath.Join(libpath, device, "dhcpd.conf")
}
d.VmwareDriver.VmnetnatConfPath = func(device string) string {
return filepath.Join(libpath, device, "nat.conf")
}
d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) {
pathNetworking := filepath.Join(libpath, "networking")
if _, err := os.Stat(pathNetworking); err != nil {
return nil, fmt.Errorf("Could not find networking conf file: %s", pathNetworking)
}
fd, err := os.Open(pathNetworking)
if err != nil {
return nil, err
}
defer fd.Close()
return ReadNetworkingConfig(fd)
}
return compareVersions(matches[1], VMWARE_FUSION_VERSION, "Fusion Professional")
}
func (d *Fusion6Driver) GetVmwareDriver() VmwareDriver {
return d.Fusion5Driver.VmwareDriver
}

View File

@ -1,6 +1,7 @@
package common
import (
"net"
"sync"
"github.com/hashicorp/packer/helper/multistep"
@ -18,11 +19,12 @@ type DriverMock struct {
CompactDiskPath string
CompactDiskErr error
CreateDiskCalled bool
CreateDiskOutput string
CreateDiskSize string
CreateDiskTypeId string
CreateDiskErr error
CreateDiskCalled bool
CreateDiskOutput string
CreateDiskSize string
CreateDiskAdapterType string
CreateDiskTypeId string
CreateDiskErr error
IsRunningCalled bool
IsRunningPath string
@ -34,6 +36,26 @@ type DriverMock struct {
CommHostResult string
CommHostErr error
HostAddressCalled bool
HostAddressState multistep.StateBag
HostAddressResult string
HostAddressErr error
HostIPCalled bool
HostIPState multistep.StateBag
HostIPResult string
HostIPErr error
GuestAddressCalled bool
GuestAddressState multistep.StateBag
GuestAddressResult string
GuestAddressErr error
GuestIPCalled bool
GuestIPState multistep.StateBag
GuestIPResult string
GuestIPErr error
StartCalled bool
StartPath string
StartHeadless bool
@ -58,10 +80,33 @@ type DriverMock struct {
DhcpLeasesPathDevice string
DhcpLeasesPathResult string
DhcpConfPathCalled bool
DhcpConfPathResult string
VmnetnatConfPathCalled bool
VmnetnatConfPathResult string
NetmapConfPathCalled bool
NetmapConfPathResult string
VerifyCalled bool
VerifyErr error
}
type NetworkMapperMock struct {
NameIntoDeviceCalled int
DeviceIntoNameCalled int
}
func (m NetworkMapperMock) NameIntoDevice(name string) (string, error) {
m.NameIntoDeviceCalled += 1
return "", nil
}
func (m NetworkMapperMock) DeviceIntoName(device string) (string, error) {
m.DeviceIntoNameCalled += 1
return "", nil
}
func (d *DriverMock) Clone(dst string, src string) error {
d.CloneCalled = true
d.CloneDst = dst
@ -75,10 +120,11 @@ func (d *DriverMock) CompactDisk(path string) error {
return d.CompactDiskErr
}
func (d *DriverMock) CreateDisk(output string, size string, typeId string) error {
func (d *DriverMock) CreateDisk(output string, size string, adapterType string, typeId string) error {
d.CreateDiskCalled = true
d.CreateDiskOutput = output
d.CreateDiskSize = size
d.CreateDiskAdapterType = adapterType
d.CreateDiskTypeId = typeId
return d.CreateDiskErr
}
@ -98,6 +144,58 @@ func (d *DriverMock) CommHost(state multistep.StateBag) (string, error) {
return d.CommHostResult, d.CommHostErr
}
func MockInterface() net.Interface {
interfaces, err := net.Interfaces()
// Build a dummy interface due to being unable to enumerate interfaces
if err != nil || len(interfaces) == 0 {
return net.Interface{
Index: 0,
MTU: -1,
Name: "dummy",
HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0},
Flags: net.FlagLoopback,
}
}
// Find the first loopback interface
for _, intf := range interfaces {
if intf.Flags&net.FlagLoopback == net.FlagLoopback {
return intf
}
}
// Fall-back to just the first one
return interfaces[0]
}
func (d *DriverMock) HostAddress(state multistep.StateBag) (string, error) {
intf := MockInterface()
d.HostAddressResult = intf.HardwareAddr.String()
d.HostAddressCalled = true
d.HostAddressState = state
return d.HostAddressResult, d.HostAddressErr
}
func (d *DriverMock) HostIP(state multistep.StateBag) (string, error) {
d.HostIPResult = "127.0.0.1"
d.HostIPCalled = true
d.HostIPState = state
return d.HostIPResult, d.HostIPErr
}
func (d *DriverMock) GuestAddress(state multistep.StateBag) (string, error) {
d.GuestAddressCalled = true
d.GuestAddressState = state
return d.GuestAddressResult, d.GuestAddressErr
}
func (d *DriverMock) GuestIP(state multistep.StateBag) (string, error) {
d.GuestIPCalled = true
d.GuestIPState = state
return d.GuestIPResult, d.GuestIPErr
}
func (d *DriverMock) Start(path string, headless bool) error {
d.StartCalled = true
d.StartPath = path
@ -134,7 +232,39 @@ func (d *DriverMock) DhcpLeasesPath(device string) string {
return d.DhcpLeasesPathResult
}
func (d *DriverMock) DhcpConfPath(device string) string {
d.DhcpConfPathCalled = true
return d.DhcpConfPathResult
}
func (d *DriverMock) VmnetnatConfPath(device string) string {
d.VmnetnatConfPathCalled = true
return d.VmnetnatConfPathResult
}
func (d *DriverMock) NetmapConfPath() string {
d.NetmapConfPathCalled = true
return d.NetmapConfPathResult
}
func (d *DriverMock) Verify() error {
d.VerifyCalled = true
return d.VerifyErr
}
func (d *DriverMock) GetVmwareDriver() VmwareDriver {
var state VmwareDriver
state.DhcpLeasesPath = func(string) string {
return "/path/to/dhcp.leases"
}
state.DhcpConfPath = func(string) string {
return "/path/to/dhcp.conf"
}
state.VmnetnatConfPath = func(string) string {
return "/path/to/vmnetnat.conf"
}
state.NetworkMapper = func() (NetworkNameMapper, error) {
return NetworkMapperMock{}, nil
}
return state
}

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,8 @@ import (
// Player5Driver is a driver that can run VMware Player 5 on Linux.
type Player5Driver struct {
VmwareDriver
AppPath string
VdiskManagerPath string
QemuImgPath string
@ -62,12 +64,12 @@ func (d *Player5Driver) qemuCompactDisk(diskPath string) error {
return nil
}
func (d *Player5Driver) CreateDisk(output string, size string, type_id string) error {
func (d *Player5Driver) CreateDisk(output string, size string, adapter_type string, type_id string) error {
var cmd *exec.Cmd
if d.QemuImgPath != "" {
cmd = exec.Command(d.QemuImgPath, "create", "-f", "vmdk", "-o", "compat6", output, size)
} else {
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", adapter_type, "-t", type_id, output)
}
if _, _, err := runAndLog(cmd); err != nil {
return err
@ -181,6 +183,33 @@ func (d *Player5Driver) Verify() error {
"One of these is required to configure disks for VMware Player.")
}
// Assigning the path callbacks to VmwareDriver
d.VmwareDriver.DhcpLeasesPath = func(device string) string {
return playerDhcpLeasesPath(device)
}
d.VmwareDriver.DhcpConfPath = func(device string) string {
return playerVmDhcpConfPath(device)
}
d.VmwareDriver.VmnetnatConfPath = func(device string) string {
return playerVmnetnatConfPath(device)
}
d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) {
pathNetmap := playerNetmapConfPath()
if _, err := os.Stat(pathNetmap); err != nil {
return nil, fmt.Errorf("Could not find netmap conf file: %s", pathNetmap)
}
fd, err := os.Open(pathNetmap)
if err != nil {
return nil, err
}
defer fd.Close()
return ReadNetworkMap(fd)
}
return nil
}
@ -192,10 +221,6 @@ func (d *Player5Driver) ToolsInstall() error {
return nil
}
func (d *Player5Driver) DhcpLeasesPath(device string) string {
return playerDhcpLeasesPath(device)
}
func (d *Player5Driver) VmnetnatConfPath() string {
return playerVmnetnatConfPath()
func (d *Player5Driver) GetVmwareDriver() VmwareDriver {
return d.VmwareDriver
}

View File

@ -57,14 +57,29 @@ func playerDhcpLeasesPath(device string) string {
} else if _, err := os.Stat(path); err == nil {
return path
}
return findFile("vmnetdhcp.leases", playerDataFilePaths())
}
func playerVmnetnatConfPath() string {
func playerVmDhcpConfPath(device string) string {
// the device isn't actually used on windows hosts
path, err := playerDhcpConfigPathRegistry()
if err != nil {
log.Printf("Error finding configuration in registry: %s", err)
} else if _, err := os.Stat(path); err == nil {
return path
}
return findFile("vmnetdhcp.conf", playerDataFilePaths())
}
func playerVmnetnatConfPath(device string) string {
// the device isn't actually used on windows hosts
return findFile("vmnetnat.conf", playerDataFilePaths())
}
func playerNetmapConfPath() string {
return findFile("netmap.conf", playerDataFilePaths())
}
// This reads the VMware installation path from the Windows registry.
func playerVMwareRoot() (s string, err error) {
key := `SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\vmplayer.exe`
@ -87,7 +102,18 @@ func playerDhcpLeasesPathRegistry() (s string, err error) {
log.Printf(`Unable to read registry key %s\%s`, key, subkey)
return
}
return normalizePath(s), nil
}
// This reads the VMware DHCP configuration path from the Windows registry.
func playerDhcpConfigPathRegistry() (s string, err error) {
key := "SYSTEM\\CurrentControlSet\\services\\VMnetDHCP\\Parameters"
subkey := "ConfFile"
s, err = readRegString(syscall.HKEY_LOCAL_MACHINE, key, subkey)
if err != nil {
log.Printf(`Unable to read registry key %s\%s`, key, subkey)
return
}
return normalizePath(s), nil
}

View File

@ -35,3 +35,7 @@ func (d *Player6Driver) Verify() error {
return playerVerifyVersion(VMWARE_PLAYER_VERSION)
}
func (d *Player6Driver) GetVmwareDriver() VmwareDriver {
return d.Player5Driver.VmwareDriver
}

View File

@ -8,6 +8,7 @@ import (
"fmt"
"log"
"os/exec"
"path/filepath"
"regexp"
"runtime"
)
@ -28,16 +29,49 @@ func playerFindVmrun() (string, error) {
return exec.LookPath("vmrun")
}
func playerDhcpLeasesPath(device string) string {
return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases"
}
func playerToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
func playerVmnetnatConfPath() string {
return ""
// return the base path to vmware's config on the host
func playerVMwareRoot() (s string, err error) {
return "/etc/vmware", nil
}
func playerDhcpLeasesPath(device string) string {
base, err := playerVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, device, "dhcpd/dhcpd.leases")
}
func playerVmDhcpConfPath(device string) string {
base, err := playerVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, device, "dhcp/dhcp.conf")
}
func playerVmnetnatConfPath(device string) string {
base, err := playerVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, device, "nat/nat.conf")
}
func playerNetmapConfPath() string {
base, err := playerVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, "netmap.conf")
}
func playerVerifyVersion(version string) error {

View File

@ -33,3 +33,7 @@ func (d *Workstation10Driver) Verify() error {
return workstationVerifyVersion(VMWARE_WS_VERSION)
}
func (d *Workstation10Driver) GetVmwareDriver() VmwareDriver {
return d.Workstation9Driver.VmwareDriver
}

View File

@ -14,6 +14,8 @@ import (
// Workstation9Driver is a driver that can run VMware Workstation 9
type Workstation9Driver struct {
VmwareDriver
AppPath string
VdiskManagerPath string
VmrunPath string
@ -40,8 +42,8 @@ func (d *Workstation9Driver) CompactDisk(diskPath string) error {
return nil
}
func (d *Workstation9Driver) CreateDisk(output string, size string, type_id string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
func (d *Workstation9Driver) CreateDisk(output string, size string, adapter_type string, type_id string) error {
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", adapter_type, "-t", type_id, output)
if _, _, err := runAndLog(cmd); err != nil {
return err
}
@ -142,6 +144,33 @@ func (d *Workstation9Driver) Verify() error {
return err
}
// Assigning the path callbacks to VmwareDriver
d.VmwareDriver.DhcpLeasesPath = func(device string) string {
return workstationDhcpLeasesPath(device)
}
d.VmwareDriver.DhcpConfPath = func(device string) string {
return workstationDhcpConfPath(device)
}
d.VmwareDriver.VmnetnatConfPath = func(device string) string {
return workstationVmnetnatConfPath(device)
}
d.VmwareDriver.NetworkMapper = func() (NetworkNameMapper, error) {
pathNetmap := workstationNetmapConfPath()
if _, err := os.Stat(pathNetmap); err != nil {
return nil, fmt.Errorf("Could not find netmap conf file: %s", pathNetmap)
}
fd, err := os.Open(pathNetmap)
if err != nil {
return nil, err
}
defer fd.Close()
return ReadNetworkMap(fd)
}
return nil
}
@ -153,10 +182,6 @@ func (d *Workstation9Driver) ToolsInstall() error {
return nil
}
func (d *Workstation9Driver) DhcpLeasesPath(device string) string {
return workstationDhcpLeasesPath(device)
}
func (d *Workstation9Driver) VmnetnatConfPath() string {
return workstationVmnetnatConfPath()
func (d *Workstation9Driver) GetVmwareDriver() VmwareDriver {
return d.VmwareDriver
}

View File

@ -59,10 +59,20 @@ func workstationDhcpLeasesPath(device string) string {
return findFile("vmnetdhcp.leases", workstationDataFilePaths())
}
func workstationVmnetnatConfPath() string {
func workstationDhcpConfPath(device string) string {
// device isn't used on a windows host
return findFile("vmnetdhcp.conf", workstationDataFilePaths())
}
func workstationVmnetnatConfPath(device string) string {
// device isn't used on a windows host
return findFile("vmnetnat.conf", workstationDataFilePaths())
}
func workstationNetmapConfPath() string {
return findFile("netmap.conf", workstationDataFilePaths())
}
// See http://blog.natefinch.com/2012/11/go-win-stuff.html
//
// This is used by workstationVMwareRoot in order to read some registry data.

View File

@ -39,18 +39,51 @@ func workstationFindVmrun() (string, error) {
return exec.LookPath("vmrun")
}
// return the base path to vmware's config on the host
func workstationVMwareRoot() (s string, err error) {
return "/etc/vmware", nil
}
func workstationDhcpLeasesPath(device string) string {
return "/etc/vmware/" + device + "/dhcpd/dhcpd.leases"
base, err := workstationVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, device, "dhcpd/dhcpd.leases")
}
func workstationDhcpConfPath(device string) string {
base, err := workstationVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, device, "dhcp/dhcpd.conf")
}
func workstationVmnetnatConfPath(device string) string {
base, err := workstationVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, device, "nat/nat.conf")
}
func workstationNetmapConfPath() string {
base, err := workstationVMwareRoot()
if err != nil {
log.Printf("Error finding VMware root: %s", err)
return ""
}
return filepath.Join(base, "netmap.conf")
}
func workstationToolsIsoPath(flavor string) string {
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
}
func workstationVmnetnatConfPath() string {
return ""
}
func workstationVerifyVersion(version string) error {
if runtime.GOOS != "linux" {
return fmt.Errorf("The VMware WS version %s driver is only supported on Linux, and Windows, at the moment. Your OS: %s", version, runtime.GOOS)

View File

@ -1,90 +0,0 @@
package common
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"regexp"
"strings"
"time"
)
// Interface to help find the IP address of a running virtual machine.
type GuestIPFinder interface {
GuestIP() (string, error)
}
// DHCPLeaseGuestLookup looks up the IP address of a guest using DHCP
// lease information from the VMware network devices.
type DHCPLeaseGuestLookup struct {
// Driver that is being used (to find leases path)
Driver Driver
// Device that the guest is connected to.
Device string
// MAC address of the guest.
MACAddress string
}
func (f *DHCPLeaseGuestLookup) GuestIP() (string, error) {
dhcpLeasesPath := f.Driver.DhcpLeasesPath(f.Device)
log.Printf("DHCP leases path: %s", dhcpLeasesPath)
if dhcpLeasesPath == "" {
return "", errors.New("no DHCP leases path found.")
}
fh, err := os.Open(dhcpLeasesPath)
if err != nil {
return "", err
}
defer fh.Close()
dhcpBytes, err := ioutil.ReadAll(fh)
if err != nil {
return "", err
}
var lastIp string
var lastLeaseEnd time.Time
var curIp string
var curLeaseEnd time.Time
ipLineRe := regexp.MustCompile(`^lease (.+?) {$`)
endTimeLineRe := regexp.MustCompile(`^\s*ends \d (.+?);$`)
macLineRe := regexp.MustCompile(`^\s*hardware ethernet (.+?);$`)
for _, line := range strings.Split(string(dhcpBytes), "\n") {
// Need to trim off CR character when running in windows
line = strings.TrimRight(line, "\r")
matches := ipLineRe.FindStringSubmatch(line)
if matches != nil {
lastIp = matches[1]
continue
}
matches = endTimeLineRe.FindStringSubmatch(line)
if matches != nil {
lastLeaseEnd, _ = time.Parse("2006/01/02 15:04:05", matches[1])
continue
}
// If the mac address matches and this lease ends farther in the
// future than the last match we might have, then choose it.
matches = macLineRe.FindStringSubmatch(line)
if matches != nil && strings.EqualFold(matches[1], f.MACAddress) && curLeaseEnd.Before(lastLeaseEnd) {
curIp = lastIp
curLeaseEnd = lastLeaseEnd
}
}
if curIp == "" {
return "", fmt.Errorf("IP not found for MAC %s in DHCP leases at %s", f.MACAddress, dhcpLeasesPath)
}
return curIp, nil
}

View File

@ -1,82 +0,0 @@
package common
import (
"io/ioutil"
"os"
"testing"
)
func TestDHCPLeaseGuestLookup_impl(t *testing.T) {
var _ GuestIPFinder = new(DHCPLeaseGuestLookup)
}
func TestDHCPLeaseGuestLookup(t *testing.T) {
tf, err := ioutil.TempFile("", "packer")
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := tf.Write([]byte(testLeaseContents)); err != nil {
t.Fatalf("err: %s", err)
}
tf.Close()
defer os.Remove(tf.Name())
driver := new(DriverMock)
driver.DhcpLeasesPathResult = tf.Name()
finder := &DHCPLeaseGuestLookup{
Driver: driver,
Device: "vmnet8",
MACAddress: "00:0c:29:59:91:02",
}
ip, err := finder.GuestIP()
if err != nil {
t.Fatalf("err: %s", err)
}
if !driver.DhcpLeasesPathCalled {
t.Fatal("should ask for DHCP leases path")
}
if driver.DhcpLeasesPathDevice != "vmnet8" {
t.Fatal("should be vmnet8")
}
if ip != "192.168.126.130" {
t.Fatalf("bad: %#v", ip)
}
}
const testLeaseContents = `
# All times in this file are in UTC (GMT), not your local timezone. This is
# not a bug, so please don't ask about it. There is no portable way to
# store leases in the local timezone, so please don't request this as a
# feature. If this is inconvenient or confusing to you, we sincerely
# apologize. Seriously, though - don't ask.
# The format of this file is documented in the dhcpd.leases(5) manual page.
lease 192.168.126.129 {
starts 0 2013/09/15 23:58:51;
ends 1 2013/09/16 00:28:51;
hardware ethernet 00:0c:29:59:91:02;
client-hostname "precise64";
}
lease 192.168.126.130 {
starts 2 2013/09/17 21:39:07;
ends 2 2013/09/17 22:09:07;
hardware ethernet 00:0c:29:59:91:02;
client-hostname "precise64";
}
lease 192.168.126.128 {
starts 0 2013/09/15 20:09:59;
ends 0 2013/09/15 20:21:58;
hardware ethernet 00:0c:29:59:91:02;
client-hostname "precise64";
}
lease 192.168.126.127 {
starts 0 2013/09/15 20:09:59;
ends 0 2013/09/15 20:21:58;
hardware ethernet 01:0c:29:59:91:02;
client-hostname "precise64";
`

View File

@ -1,7 +0,0 @@
package common
// Interface to help find the host IP that is available from within
// the VMware virtual machines.
type HostIPFinder interface {
HostIP() (string, error)
}

View File

@ -1,11 +0,0 @@
package common
import "testing"
func TestIfconfigIPFinder_Impl(t *testing.T) {
var raw interface{}
raw = &IfconfigIPFinder{}
if _, ok := raw.(HostIPFinder); !ok {
t.Fatalf("IfconfigIPFinder is not a host IP finder")
}
}

View File

@ -1,65 +0,0 @@
package common
import (
"bufio"
"errors"
"fmt"
"io"
"os"
"regexp"
"strings"
)
// VMnetNatConfIPFinder finds the IP address of the host machine by
// retrieving the IP from the vmnetnat.conf. This isn't a full proof
// technique but so far it has not failed.
type VMnetNatConfIPFinder struct{}
func (*VMnetNatConfIPFinder) HostIP() (string, error) {
driver := &Workstation9Driver{}
vmnetnat := driver.VmnetnatConfPath()
if vmnetnat == "" {
return "", errors.New("Could not find NAT vmnet conf file")
}
if _, err := os.Stat(vmnetnat); err != nil {
return "", fmt.Errorf("Could not find NAT vmnet conf file: %s", vmnetnat)
}
f, err := os.Open(vmnetnat)
if err != nil {
return "", err
}
defer f.Close()
ipRe := regexp.MustCompile(`^\s*ip\s*=\s*(.+?)\s*$`)
r := bufio.NewReader(f)
for {
line, err := r.ReadString('\n')
if line != "" {
matches := ipRe.FindStringSubmatch(line)
if matches != nil {
ip := matches[1]
dotIndex := strings.LastIndex(ip, ".")
if dotIndex == -1 {
continue
}
ip = ip[0:dotIndex] + ".1"
return ip, nil
}
}
if err == io.EOF {
break
}
if err != nil {
return "", err
}
}
return "", errors.New("host IP not found in " + vmnetnat)
}

View File

@ -1,11 +0,0 @@
package common
import "testing"
func TestVMnetNatConfIPFinder_Impl(t *testing.T) {
var raw interface{}
raw = &VMnetNatConfIPFinder{}
if _, ok := raw.(HostIPFinder); !ok {
t.Fatalf("VMnetNatConfIPFinder is not a host IP finder")
}
}

View File

@ -3,9 +3,7 @@ package common
import (
"errors"
"fmt"
"io/ioutil"
"log"
"os"
commonssh "github.com/hashicorp/packer/common/ssh"
"github.com/hashicorp/packer/communicator/ssh"
@ -16,41 +14,12 @@ import (
func CommHost(config *SSHConfig) func(multistep.StateBag) (string, error) {
return func(state multistep.StateBag) (string, error) {
driver := state.Get("driver").(Driver)
vmxPath := state.Get("vmx_path").(string)
if config.Comm.SSHHost != "" {
return config.Comm.SSHHost, nil
}
log.Println("Lookup up IP information...")
f, err := os.Open(vmxPath)
if err != nil {
return "", err
}
defer f.Close()
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
return "", err
}
vmxData := ParseVMX(string(vmxBytes))
var ok bool
macAddress := ""
if macAddress, ok = vmxData["ethernet0.address"]; !ok || macAddress == "" {
if macAddress, ok = vmxData["ethernet0.generatedaddress"]; !ok || macAddress == "" {
return "", errors.New("couldn't find MAC address in VMX")
}
}
ipLookup := &DHCPLeaseGuestLookup{
Driver: driver,
Device: "vmnet8",
MACAddress: macAddress,
}
ipAddress, err := ipLookup.GuestIP()
ipAddress, err := driver.GuestIP(state)
if err != nil {
log.Printf("IP lookup failed: %s", err)
return "", fmt.Errorf("IP lookup failed: %s", err)

View File

@ -3,7 +3,6 @@ package common
import (
"context"
"fmt"
"io/ioutil"
"log"
"regexp"
"strings"
@ -26,7 +25,7 @@ func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) mult
ui := state.Get("ui").(packer.Ui)
vmxPath := state.Get("vmx_path").(string)
vmxContents, err := ioutil.ReadFile(vmxPath)
vmxData, err := ReadVMX(vmxPath)
if err != nil {
err := fmt.Errorf("Error reading VMX file: %s", err)
state.Put("error", err)
@ -34,8 +33,6 @@ func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionHalt
}
vmxData := ParseVMX(string(vmxContents))
// Set this so that no dialogs ever appear from Packer.
vmxData["msg.autoanswer"] = "true"

View File

@ -3,11 +3,9 @@ package common
import (
"context"
"fmt"
"io/ioutil"
"log"
"math/rand"
"net"
"os"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
@ -87,17 +85,9 @@ func (s *StepConfigureVNC) Run(_ context.Context, state multistep.StateBag) mult
ui := state.Get("ui").(packer.Ui)
vmxPath := state.Get("vmx_path").(string)
f, err := os.Open(vmxPath)
vmxData, err := ReadVMX(vmxPath)
if err != nil {
err := fmt.Errorf("Error reading VMX data: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
vmxBytes, err := ioutil.ReadAll(f)
if err != nil {
err := fmt.Errorf("Error reading VMX data: %s", err)
err := fmt.Errorf("Error reading VMX file: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
@ -121,7 +111,6 @@ func (s *StepConfigureVNC) Run(_ context.Context, state multistep.StateBag) mult
log.Printf("Found available VNC port: %d", vncPort)
vmxData := ParseVMX(string(vmxBytes))
vncFinder.UpdateVMX(vncBindAddress, vncPassword, vncPort, vmxData)
if err := WriteVMX(vmxPath, vmxData); err != nil {

View File

@ -116,6 +116,10 @@ func TestStepShutdown_noCommand(t *testing.T) {
}
func TestStepShutdown_locks(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.")
}
state := testStepShutdownState(t)
step := new(StepShutdown)
step.Testing = true

View File

@ -6,7 +6,7 @@ import (
"log"
"net"
"os"
"runtime"
"regexp"
"strings"
"time"
"unicode"
@ -63,7 +63,7 @@ func (s *StepTypeBootCommand) Run(_ context.Context, state multistep.StateBag) m
}
// Connect to VNC
ui.Say("Connecting to VM via VNC")
ui.Say(fmt.Sprintf("Connecting to VM via VNC (%s:%d)", vncIp, vncPort))
nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIp, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
@ -93,16 +93,7 @@ func (s *StepTypeBootCommand) Run(_ context.Context, state multistep.StateBag) m
log.Printf("Connected to VNC desktop: %s", c.DesktopName)
// Determine the host IP
var ipFinder HostIPFinder
if finder, ok := driver.(HostIPFinder); ok {
ipFinder = finder
} else if runtime.GOOS == "windows" {
ipFinder = new(VMnetNatConfIPFinder)
} else {
ipFinder = &IfconfigIPFinder{Device: "vmnet8"}
}
hostIP, err := ipFinder.HostIP()
hostIP, err := driver.HostIP(state)
if err != nil {
err := fmt.Errorf("Error detecting host IP: %s", err)
state.Put("error", err)
@ -184,6 +175,8 @@ func vncSendString(c *vnc.ClientConn, original string) {
special["<rightAlt>"] = 0xFFEA
special["<rightCtrl>"] = 0xFFE4
special["<rightShift>"] = 0xFFE2
special["<leftSuper>"] = 0xFFEB
special["<rightSuper>"] = 0xFFEC
shiftedChars := "~!@#$%^&*()_+{}|:\"<>?"
@ -194,6 +187,9 @@ func vncSendString(c *vnc.ClientConn, original string) {
keyInterval = delay
}
azOnRegex := regexp.MustCompile("^<(?P<ordinary>[a-zA-Z])On>")
azOffRegex := regexp.MustCompile("^<(?P<ordinary>[a-zA-Z])Off>")
// TODO(mitchellh): Ripe for optimizations of some point, perhaps.
for len(original) > 0 {
var keyCode uint32
@ -232,6 +228,36 @@ func vncSendString(c *vnc.ClientConn, original string) {
continue
}
if strings.HasPrefix(original, "<leftSuperOn>") {
keyCode = special["<leftSuper>"]
original = original[len("<leftSuperOn>"):]
log.Printf("Special code '<leftSuperOn>' found, replacing with: %d", keyCode)
c.KeyEvent(keyCode, true)
time.Sleep(keyInterval)
continue
}
if azOnRegex.MatchString(original) {
m := azOnRegex.FindStringSubmatch(original)
r, _ := utf8.DecodeRuneInString(m[1])
original = original[len("<aOn>"):]
keyCode = uint32(r)
keyShift = unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r)
log.Printf("Special code '%s' found, replacing with %d, shift %v", m[0], keyCode, keyShift)
if keyShift {
c.KeyEvent(KeyLeftShift, true)
}
c.KeyEvent(keyCode, true)
time.Sleep(keyInterval)
continue
}
if strings.HasPrefix(original, "<leftAltOff>") {
keyCode = special["<leftAlt>"]
original = original[len("<leftAltOff>"):]
@ -265,6 +291,36 @@ func vncSendString(c *vnc.ClientConn, original string) {
continue
}
if strings.HasPrefix(original, "<leftSuperOff>") {
keyCode = special["<leftSuper>"]
original = original[len("<leftSuperOff>"):]
log.Printf("Special code '<leftSuperOff>' found, replacing with: %d", keyCode)
c.KeyEvent(keyCode, false)
time.Sleep(keyInterval)
continue
}
if azOffRegex.MatchString(original) {
m := azOffRegex.FindStringSubmatch(original)
r, _ := utf8.DecodeRuneInString(m[1])
original = original[len("<aOff>"):]
keyCode = uint32(r)
keyShift = unicode.IsUpper(r) || strings.ContainsRune(shiftedChars, r)
log.Printf("Special code '%s' found, replacing with %d, shift %v", m[0], keyCode, keyShift)
if keyShift {
c.KeyEvent(KeyLeftShift, false)
}
c.KeyEvent(keyCode, false)
time.Sleep(keyInterval)
continue
}
if strings.HasPrefix(original, "<rightAltOn>") {
keyCode = special["<rightAlt>"]
original = original[len("<rightAltOn>"):]
@ -298,6 +354,17 @@ func vncSendString(c *vnc.ClientConn, original string) {
continue
}
if strings.HasPrefix(original, "<rightSuperOn>") {
keyCode = special["<rightSuper>"]
original = original[len("<rightSuperOn>"):]
log.Printf("Special code '<rightSuperOn>' found, replacing with: %d", keyCode)
c.KeyEvent(keyCode, true)
time.Sleep(keyInterval)
continue
}
if strings.HasPrefix(original, "<rightAltOff>") {
keyCode = special["<rightAlt>"]
original = original[len("<rightAltOff>"):]
@ -331,6 +398,17 @@ func vncSendString(c *vnc.ClientConn, original string) {
continue
}
if strings.HasPrefix(original, "<rightSuperOff>") {
keyCode = special["<rightSuper>"]
original = original[len("<rightSuperOff>"):]
log.Printf("Special code '<rightSuperOff>' found, replacing with: %d", keyCode)
c.KeyEvent(keyCode, false)
time.Sleep(keyInterval)
continue
}
if strings.HasPrefix(original, "<wait>") {
log.Printf("Special code '<wait>' found, sleeping one second")
time.Sleep(1 * time.Second)

View File

@ -38,21 +38,43 @@ type Config struct {
vmwcommon.ToolsConfig `mapstructure:",squash"`
vmwcommon.VMXConfig `mapstructure:",squash"`
AdditionalDiskSize []uint `mapstructure:"disk_additional_size"`
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
DiskTypeId string `mapstructure:"disk_type_id"`
Format string `mapstructure:"format"`
GuestOSType string `mapstructure:"guest_os_type"`
// disk drives
AdditionalDiskSize []uint `mapstructure:"disk_additional_size"`
DiskAdapterType string `mapstructure:"disk_adapter_type"`
DiskName string `mapstructure:"vmdk_name"`
DiskSize uint `mapstructure:"disk_size"`
DiskTypeId string `mapstructure:"disk_type_id"`
Format string `mapstructure:"format"`
// cdrom drive
CdromAdapterType string `mapstructure:"cdrom_adapter_type"`
// platform information
GuestOSType string `mapstructure:"guest_os_type"`
Version string `mapstructure:"version"`
VMName string `mapstructure:"vm_name"`
// Network adapter and type
NetworkAdapterType string `mapstructure:"network_adapter_type"`
Network string `mapstructure:"network"`
// device presence
Sound bool `mapstructure:"sound"`
USB bool `mapstructure:"usb"`
// communication ports
Serial string `mapstructure:"serial"`
Parallel string `mapstructure:"parallel"`
// booting a guest
KeepRegistered bool `mapstructure:"keep_registered"`
OVFToolOptions []string `mapstructure:"ovftool_options"`
SkipCompaction bool `mapstructure:"skip_compaction"`
SkipExport bool `mapstructure:"skip_export"`
VMName string `mapstructure:"vm_name"`
VMXDiskTemplatePath string `mapstructure:"vmx_disk_template_path"`
VMXTemplatePath string `mapstructure:"vmx_template_path"`
Version string `mapstructure:"version"`
// remote vsphere
RemoteType string `mapstructure:"remote_type"`
RemoteDatastore string `mapstructure:"remote_datastore"`
RemoteCacheDatastore string `mapstructure:"remote_cache_datastore"`
@ -109,6 +131,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.DiskSize = 40000
}
if b.config.DiskAdapterType == "" {
// Default is lsilogic
b.config.DiskAdapterType = "lsilogic"
}
if b.config.DiskTypeId == "" {
// Default is growable virtual disk split in 2GB files.
b.config.DiskTypeId = "1"
@ -158,6 +185,18 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
}
if b.config.Network == "" {
b.config.Network = "nat"
}
if !b.config.Sound {
b.config.Sound = false
}
if !b.config.USB {
b.config.USB = false
}
// Remote configuration validation
if b.config.RemoteType != "" {
if b.config.RemoteHost == "" {

View File

@ -14,6 +14,7 @@ import (
"strings"
"time"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
commonssh "github.com/hashicorp/packer/common/ssh"
"github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/multistep"
@ -24,6 +25,8 @@ import (
// ESX5 driver talks to an ESXi5 hypervisor remotely over SSH to build
// virtual machines. This driver can only manage one machine at a time.
type ESX5Driver struct {
base vmwcommon.VmwareDriver
Host string
Port uint
Username string
@ -46,9 +49,9 @@ func (d *ESX5Driver) CompactDisk(diskPathLocal string) error {
return nil
}
func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, typeId string) error {
func (d *ESX5Driver) CreateDisk(diskPathLocal string, size string, adapter_type string, typeId string) error {
diskPath := d.datastorePath(diskPathLocal)
return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", "lsilogic", diskPath)
return d.sh("vmkfstools", "-c", size, "-d", typeId, "-a", adapter_type, diskPath)
}
func (d *ESX5Driver) IsRunning(string) (bool, error) {
@ -143,10 +146,6 @@ func (d *ESX5Driver) ToolsInstall() error {
return d.sh("vim-cmd", "vmsvc/tools.install", d.vmId)
}
func (d *ESX5Driver) DhcpLeasesPath(string) string {
return ""
}
func (d *ESX5Driver) Verify() error {
checks := []func() error{
d.connect,
@ -159,11 +158,10 @@ func (d *ESX5Driver) Verify() error {
return err
}
}
return nil
}
func (d *ESX5Driver) HostIP() (string, error) {
func (d *ESX5Driver) HostIP(multistep.StateBag) (string, error) {
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port))
if err != nil {
return "", err
@ -174,6 +172,101 @@ func (d *ESX5Driver) HostIP() (string, error) {
return host, err
}
func (d *ESX5Driver) GuestIP(multistep.StateBag) (string, error) {
// GuestIP is defined by the user as d.Host..but let's validate it just to be sure
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port))
defer conn.Close()
if err != nil {
return "", err
}
host, _, err := net.SplitHostPort(conn.RemoteAddr().String())
return host, err
}
func (d *ESX5Driver) HostAddress(multistep.StateBag) (string, error) {
// make a connection
conn, err := net.Dial("tcp", fmt.Sprintf("%s:%d", d.Host, d.Port))
defer conn.Close()
if err != nil {
return "", err
}
// get the local address (the host)
host, _, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
return "", fmt.Errorf("Unable to determine host address for ESXi: %v", err)
}
// iterate through all the interfaces..
interfaces, err := net.Interfaces()
if err != nil {
return "", fmt.Errorf("Unable to enumerate host interfaces : %v", err)
}
for _, intf := range interfaces {
addrs, err := intf.Addrs()
if err != nil {
continue
}
// ..checking to see if any if it's addrs match the host address
for _, addr := range addrs {
if addr.String() == host { // FIXME: Is this the proper way to compare two HardwareAddrs?
return intf.HardwareAddr.String(), nil
}
}
}
// ..unfortunately nothing was found
return "", fmt.Errorf("Unable to locate interface matching host address in ESXi: %v", host)
}
func (d *ESX5Driver) GuestAddress(multistep.StateBag) (string, error) {
// list all the interfaces on the esx host
r, err := d.esxcli("network", "ip", "interface", "list")
if err != nil {
return "", fmt.Errorf("Could not retrieve network interfaces for ESXi: %v", err)
}
// rip out the interface name and the MAC address from the csv output
addrs := make(map[string]string)
for record, err := r.read(); record != nil && err == nil; record, err = r.read() {
if strings.ToUpper(record["Enabled"]) != "TRUE" {
continue
}
addrs[record["Name"]] = record["MAC Address"]
}
// list all the addresses on the esx host
r, err = d.esxcli("network", "ip", "interface", "ipv4", "get")
if err != nil {
return "", fmt.Errorf("Could not retrieve network addresses for ESXi: %v", err)
}
// figure out the interface name that matches the specified d.Host address
var intf string
intf = ""
for record, err := r.read(); record != nil && err == nil; record, err = r.read() {
if record["IPv4 Address"] == d.Host && record["Name"] != "" {
intf = record["Name"]
break
}
}
if intf == "" {
return "", fmt.Errorf("Unable to find matching address for ESXi guest")
}
// find the MAC address according to the interface name
result, ok := addrs[intf]
if !ok {
return "", fmt.Errorf("Unable to find address for ESXi guest interface")
}
// ..and we're good
return result, nil
}
func (d *ESX5Driver) VNCAddress(_ string, portMin, portMax uint) (string, uint, error) {
var vncPort uint
@ -531,6 +624,10 @@ func (d *ESX5Driver) esxcli(args ...string) (*esxcliReader, error) {
return &esxcliReader{r, header}, nil
}
func (d *ESX5Driver) GetVmwareDriver() vmwcommon.VmwareDriver {
return d.base
}
type esxcliReader struct {
cr *csv.Reader
header []string

View File

@ -50,8 +50,9 @@ func TestESX5Driver_HostIP(t *testing.T) {
defer listen.Close()
driver := ESX5Driver{Host: "localhost", Port: uint(port)}
state := new(multistep.BasicStateBag)
if host, _ := driver.HostIP(); host != expected_host {
if host, _ := driver.HostIP(state); host != expected_host {
t.Error(fmt.Sprintf("Expected string, %s but got %s", expected_host, host))
}
}

View File

@ -28,7 +28,7 @@ func (stepCreateDisk) Run(_ context.Context, state multistep.StateBag) multistep
ui.Say("Creating virtual machine disk")
full_disk_path := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize), config.DiskTypeId); err != nil {
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize), config.DiskAdapterType, config.DiskTypeId); err != nil {
err := fmt.Errorf("Error creating disk: %s", err)
state.Put("error", err)
ui.Error(err.Error())
@ -46,7 +46,7 @@ func (stepCreateDisk) Run(_ context.Context, state multistep.StateBag) multistep
additionalpath := filepath.Join(config.OutputDir, fmt.Sprintf("%s-%d.vmdk", config.DiskName, i+1))
size := fmt.Sprintf("%dM", uint64(additionalsize))
if err := driver.CreateDisk(additionalpath, size, config.DiskTypeId); err != nil {
if err := driver.CreateDisk(additionalpath, size, config.DiskAdapterType, config.DiskTypeId); err != nil {
err := fmt.Errorf("Error creating additional disk: %s", err)
state.Put("error", err)
ui.Error(err.Error())

View File

@ -6,6 +6,8 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"strings"
vmwcommon "github.com/hashicorp/packer/builder/vmware/common"
"github.com/hashicorp/packer/helper/multistep"
@ -14,11 +16,40 @@ import (
)
type vmxTemplateData struct {
Name string
GuestOS string
DiskName string
ISOPath string
Version string
Name string
GuestOS string
ISOPath string
Version string
SCSI_Present string
SCSI_diskAdapterType string
SATA_Present string
NVME_Present string
DiskName string
DiskType string
CDROMType string
CDROMType_MasterSlave string
Network_Type string
Network_Device string
Network_Adapter string
Sound_Present string
Usb_Present string
Serial_Present string
Serial_Type string
Serial_Endpoint string
Serial_Host string
Serial_Yield string
Serial_Filename string
Serial_Auto string
Parallel_Present string
Parallel_Bidirectional string
Parallel_Filename string
Parallel_Auto string
}
type additionalDiskTemplateData struct {
@ -39,11 +70,238 @@ type stepCreateVMX struct {
tempDir string
}
/* serial conversions */
type serialConfigPipe struct {
filename string
endpoint string
host string
yield string
}
type serialConfigFile struct {
filename string
yield string
}
type serialConfigDevice struct {
devicename string
yield string
}
type serialConfigAuto struct {
devicename string
yield string
}
type serialUnion struct {
serialType interface{}
pipe *serialConfigPipe
file *serialConfigFile
device *serialConfigDevice
auto *serialConfigAuto
}
func unformat_serial(config string) (*serialUnion, error) {
var defaultSerialPort string
if runtime.GOOS == "windows" {
defaultSerialPort = "COM1"
} else {
defaultSerialPort = "/dev/ttyS0"
}
input := strings.SplitN(config, ":", 2)
if len(input) < 1 {
return nil, fmt.Errorf("Unexpected format for serial port: %s", config)
}
var formatType, formatOptions string
formatType = input[0]
if len(input) == 2 {
formatOptions = input[1]
} else {
formatOptions = ""
}
switch strings.ToUpper(formatType) {
case "PIPE":
comp := strings.Split(formatOptions, ",")
if len(comp) < 3 || len(comp) > 4 {
return nil, fmt.Errorf("Unexpected format for serial port : pipe : %s", config)
}
if res := strings.ToLower(comp[1]); res != "client" && res != "server" {
return nil, fmt.Errorf("Unexpected format for serial port : pipe : endpoint : %s : %s", res, config)
}
if res := strings.ToLower(comp[2]); res != "app" && res != "vm" {
return nil, fmt.Errorf("Unexpected format for serial port : pipe : host : %s : %s", res, config)
}
res := &serialConfigPipe{
filename: comp[0],
endpoint: comp[1],
host: map[string]string{"app": "TRUE", "vm": "FALSE"}[strings.ToLower(comp[2])],
yield: "FALSE",
}
if len(comp) == 4 {
res.yield = strings.ToUpper(comp[3])
}
if res.yield != "TRUE" && res.yield != "FALSE" {
return nil, fmt.Errorf("Unexpected format for serial port : pipe : yield : %s : %s", res.yield, config)
}
return &serialUnion{serialType: res, pipe: res}, nil
case "FILE":
comp := strings.Split(formatOptions, ",")
if len(comp) > 2 {
return nil, fmt.Errorf("Unexpected format for serial port : file : %s", config)
}
res := &serialConfigFile{yield: "FALSE"}
res.filename = filepath.FromSlash(comp[0])
res.yield = map[bool]string{true: strings.ToUpper(comp[0]), false: "FALSE"}[len(comp) > 1]
if res.yield != "TRUE" && res.yield != "FALSE" {
return nil, fmt.Errorf("Unexpected format for serial port : file : yield : %s : %s", res.yield, config)
}
return &serialUnion{serialType: res, file: res}, nil
case "DEVICE":
comp := strings.Split(formatOptions, ",")
if len(comp) > 2 {
return nil, fmt.Errorf("Unexpected format for serial port : device : %s", config)
}
res := new(serialConfigDevice)
if len(comp) == 2 {
res.devicename = map[bool]string{true: filepath.FromSlash(comp[0]), false: defaultSerialPort}[len(comp[0]) > 0]
res.yield = strings.ToUpper(comp[1])
} else if len(comp) == 1 {
res.devicename = map[bool]string{true: filepath.FromSlash(comp[0]), false: defaultSerialPort}[len(comp[0]) > 0]
res.yield = "FALSE"
} else if len(comp) == 0 {
res.devicename = defaultSerialPort
res.yield = "FALSE"
}
if res.yield != "TRUE" && res.yield != "FALSE" {
return nil, fmt.Errorf("Unexpected format for serial port : device : yield : %s : %s", res.yield, config)
}
return &serialUnion{serialType: res, device: res}, nil
case "AUTO":
res := new(serialConfigAuto)
res.devicename = defaultSerialPort
if len(formatOptions) > 0 {
res.yield = strings.ToUpper(formatOptions)
} else {
res.yield = "FALSE"
}
if res.yield != "TRUE" && res.yield != "FALSE" {
return nil, fmt.Errorf("Unexpected format for serial port : auto : yield : %s : %s", res.yield, config)
}
return &serialUnion{serialType: res, auto: res}, nil
case "NONE":
return &serialUnion{serialType: nil}, nil
default:
return nil, fmt.Errorf("Unknown serial type : %s : %s", strings.ToUpper(formatType), config)
}
}
/* parallel port */
type parallelUnion struct {
parallelType interface{}
file *parallelPortFile
device *parallelPortDevice
auto *parallelPortAuto
}
type parallelPortFile struct {
filename string
}
type parallelPortDevice struct {
bidirectional string
devicename string
}
type parallelPortAuto struct {
bidirectional string
}
func unformat_parallel(config string) (*parallelUnion, error) {
input := strings.SplitN(config, ":", 2)
if len(input) < 1 {
return nil, fmt.Errorf("Unexpected format for parallel port: %s", config)
}
var formatType, formatOptions string
formatType = input[0]
if len(input) == 2 {
formatOptions = input[1]
} else {
formatOptions = ""
}
switch strings.ToUpper(formatType) {
case "FILE":
res := &parallelPortFile{filename: filepath.FromSlash(formatOptions)}
return &parallelUnion{parallelType: res, file: res}, nil
case "DEVICE":
comp := strings.Split(formatOptions, ",")
if len(comp) < 1 || len(comp) > 2 {
return nil, fmt.Errorf("Unexpected format for parallel port: %s", config)
}
res := new(parallelPortDevice)
res.bidirectional = "FALSE"
res.devicename = filepath.FromSlash(comp[0])
if len(comp) > 1 {
switch strings.ToUpper(comp[1]) {
case "BI":
res.bidirectional = "TRUE"
case "UNI":
res.bidirectional = "FALSE"
default:
return nil, fmt.Errorf("Unknown parallel port direction : %s : %s", strings.ToUpper(comp[0]), config)
}
}
return &parallelUnion{parallelType: res, device: res}, nil
case "AUTO":
res := new(parallelPortAuto)
switch strings.ToUpper(formatOptions) {
case "":
fallthrough
case "UNI":
res.bidirectional = "FALSE"
case "BI":
res.bidirectional = "TRUE"
default:
return nil, fmt.Errorf("Unknown parallel port direction : %s : %s", strings.ToUpper(formatOptions), config)
}
return &parallelUnion{parallelType: res, auto: res}, nil
case "NONE":
return &parallelUnion{parallelType: nil}, nil
}
return nil, fmt.Errorf("Unexpected format for parallel port: %s", config)
}
/* regular steps */
func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
isoPath := state.Get("iso_path").(string)
ui := state.Get("ui").(packer.Ui)
// Convert the iso_path into a path relative to the .vmx file if possible
if relativeIsoPath, err := filepath.Rel(config.VMXTemplatePath, filepath.FromSlash(isoPath)); err == nil {
isoPath = relativeIsoPath
}
ui.Say("Building and writing VMX file")
vmxTemplate := DefaultVMXTemplate
@ -111,14 +369,211 @@ func (s *stepCreateVMX) Run(_ context.Context, state multistep.StateBag) multist
}
}
ctx.Data = &vmxTemplateData{
templateData := vmxTemplateData{
Name: config.VMName,
GuestOS: config.GuestOSType,
DiskName: config.DiskName,
Version: config.Version,
ISOPath: isoPath,
SCSI_Present: "FALSE",
SCSI_diskAdapterType: "lsilogic",
SATA_Present: "FALSE",
NVME_Present: "FALSE",
DiskType: "scsi",
CDROMType: "ide",
CDROMType_MasterSlave: "0",
Network_Adapter: "e1000",
Sound_Present: map[bool]string{true: "TRUE", false: "FALSE"}[bool(config.Sound)],
Usb_Present: map[bool]string{true: "TRUE", false: "FALSE"}[bool(config.USB)],
Serial_Present: "FALSE",
Parallel_Present: "FALSE",
}
/// Use the disk adapter type that the user specified to tweak the .vmx
// Also sync the cdrom adapter type according to what's common for that disk type.
diskAdapterType := strings.ToLower(config.DiskAdapterType)
switch diskAdapterType {
case "ide":
templateData.DiskType = "ide"
templateData.CDROMType = "ide"
templateData.CDROMType_MasterSlave = "1"
case "sata":
templateData.SATA_Present = "TRUE"
templateData.DiskType = "sata"
templateData.CDROMType = "sata"
templateData.CDROMType_MasterSlave = "1"
case "nvme":
templateData.NVME_Present = "TRUE"
templateData.DiskType = "nvme"
templateData.SATA_Present = "TRUE"
templateData.CDROMType = "sata"
templateData.CDROMType_MasterSlave = "0"
case "scsi":
diskAdapterType = "lsilogic"
fallthrough
default:
templateData.SCSI_Present = "TRUE"
templateData.SCSI_diskAdapterType = diskAdapterType
templateData.DiskType = "scsi"
templateData.CDROMType = "ide"
templateData.CDROMType_MasterSlave = "0"
}
/// Handle the cdrom adapter type. If the disk adapter type and the
// cdrom adapter type are the same, then ensure that the cdrom is the
// slave device on whatever bus the disk adapter is on.
cdromAdapterType := strings.ToLower(config.CdromAdapterType)
if cdromAdapterType == "" {
cdromAdapterType = templateData.CDROMType
} else if cdromAdapterType == diskAdapterType {
templateData.CDROMType_MasterSlave = "1"
} else {
templateData.CDROMType_MasterSlave = "0"
}
switch cdromAdapterType {
case "ide":
templateData.CDROMType = "ide"
case "sata":
templateData.SATA_Present = "TRUE"
templateData.CDROMType = "sata"
case "scsi":
templateData.SCSI_Present = "TRUE"
templateData.CDROMType = "scsi"
default:
err := fmt.Errorf("Error procesing VMX template: %s", cdromAdapterType)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
/// Assign the network adapter type into the template if one was specified.
network_adapter := strings.ToLower(config.NetworkAdapterType)
if network_adapter != "" {
templateData.Network_Adapter = network_adapter
}
/// Check the network type that the user specified
network := config.Network
driver := state.Get("driver").(vmwcommon.Driver).GetVmwareDriver()
// read netmap config
netmap, err := driver.NetworkMapper()
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// try and convert the specified network to a device
device, err := netmap.NameIntoDevice(network)
// success. so we know that it's an actual network type inside netmap.conf
if err == nil {
templateData.Network_Type = network
templateData.Network_Device = device
// we were unable to find the type, so assume it's a custom network device.
} else {
templateData.Network_Type = "custom"
templateData.Network_Device = network
}
// store the network so that we can later figure out what ip address to bind to
state.Put("vmnetwork", network)
/// check if serial port has been configured
if config.Serial == "" {
templateData.Serial_Present = "FALSE"
} else {
serial, err := unformat_serial(config.Serial)
if err != nil {
err := fmt.Errorf("Error procesing VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
templateData.Serial_Present = "TRUE"
templateData.Serial_Filename = ""
templateData.Serial_Yield = ""
templateData.Serial_Endpoint = ""
templateData.Serial_Host = ""
templateData.Serial_Auto = "FALSE"
switch serial.serialType.(type) {
case *serialConfigPipe:
templateData.Serial_Type = "pipe"
templateData.Serial_Endpoint = serial.pipe.endpoint
templateData.Serial_Host = serial.pipe.host
templateData.Serial_Yield = serial.pipe.yield
templateData.Serial_Filename = filepath.FromSlash(serial.pipe.filename)
case *serialConfigFile:
templateData.Serial_Type = "file"
templateData.Serial_Filename = filepath.FromSlash(serial.file.filename)
case *serialConfigDevice:
templateData.Serial_Type = "device"
templateData.Serial_Filename = filepath.FromSlash(serial.device.devicename)
case *serialConfigAuto:
templateData.Serial_Type = "device"
templateData.Serial_Filename = filepath.FromSlash(serial.auto.devicename)
templateData.Serial_Yield = serial.auto.yield
templateData.Serial_Auto = "TRUE"
case nil:
templateData.Serial_Present = "FALSE"
break
default:
err := fmt.Errorf("Error procesing VMX template: %v", serial)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
/// check if parallel port has been configured
if config.Parallel == "" {
templateData.Parallel_Present = "FALSE"
} else {
parallel, err := unformat_parallel(config.Parallel)
if err != nil {
err := fmt.Errorf("Error procesing VMX template: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
templateData.Parallel_Auto = "FALSE"
switch parallel.parallelType.(type) {
case *parallelPortFile:
templateData.Parallel_Present = "TRUE"
templateData.Parallel_Filename = filepath.FromSlash(parallel.file.filename)
case *parallelPortDevice:
templateData.Parallel_Present = "TRUE"
templateData.Parallel_Bidirectional = parallel.device.bidirectional
templateData.Parallel_Filename = filepath.FromSlash(parallel.device.devicename)
case *parallelPortAuto:
templateData.Parallel_Present = "TRUE"
templateData.Parallel_Auto = "TRUE"
templateData.Parallel_Bidirectional = parallel.auto.bidirectional
case nil:
templateData.Parallel_Present = "FALSE"
break
default:
err := fmt.Errorf("Error procesing VMX template: %v", parallel)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
ctx.Data = &templateData
/// render the .vmx template
vmxContents, err := interpolate.Render(vmxTemplate, &ctx)
if err != nil {
err := fmt.Errorf("Error procesing VMX template: %s", err)
@ -176,12 +631,13 @@ ehci.pciSlotNumber = "34"
ehci.present = "TRUE"
ethernet0.addressType = "generated"
ethernet0.bsdName = "en0"
ethernet0.connectionType = "nat"
ethernet0.connectionType = "{{ .Network_Type }}"
ethernet0.vnet = "{{ .Network_Device }}"
ethernet0.displayName = "Ethernet"
ethernet0.linkStatePropagation.enable = "FALSE"
ethernet0.pciSlotNumber = "33"
ethernet0.present = "TRUE"
ethernet0.virtualDev = "e1000"
ethernet0.virtualDev = "{{ .Network_Adapter }}"
ethernet0.wakeOnPcktRcv = "FALSE"
extendedConfigFile = "{{ .Name }}.vmxf"
floppy0.present = "FALSE"
@ -190,9 +646,21 @@ gui.fullScreenAtPowerOn = "FALSE"
gui.viewModeAtPowerOn = "windowed"
hgfs.linkRootShare = "TRUE"
hgfs.mapRootShare = "TRUE"
ide1:0.present = "TRUE"
ide1:0.fileName = "{{ .ISOPath }}"
ide1:0.deviceType = "cdrom-image"
scsi0.present = "{{ .SCSI_Present }}"
scsi0.virtualDev = "{{ .SCSI_diskAdapterType }}"
scsi0.pciSlotNumber = "16"
scsi0:0.redo = ""
sata0.present = "{{ .SATA_Present }}"
nvme0.present = "{{ .NVME_Present }}"
{{ .DiskType }}0:0.present = "TRUE"
{{ .DiskType }}0:0.fileName = "{{ .DiskName }}.vmdk"
{{ .CDROMType }}0:{{ .CDROMType_MasterSlave }}.present = "TRUE"
{{ .CDROMType }}0:{{ .CDROMType_MasterSlave }}.fileName = "{{ .ISOPath }}"
{{ .CDROMType }}0:{{ .CDROMType_MasterSlave }}.deviceType = "cdrom-image"
isolation.tools.hgfs.disable = "FALSE"
memsize = "512"
nvram = "{{ .Name }}.nvram"
@ -221,17 +689,38 @@ powerType.suspend = "soft"
proxyApps.publishToHost = "FALSE"
replay.filename = ""
replay.supported = "FALSE"
scsi0.pciSlotNumber = "16"
scsi0.present = "TRUE"
scsi0.virtualDev = "lsilogic"
scsi0:0.fileName = "{{ .DiskName }}.vmdk"
scsi0:0.present = "TRUE"
scsi0:0.redo = ""
sound.startConnected = "FALSE"
// Sound
sound.startConnected = "{{ .Sound_Present }}"
sound.present = "{{ .Sound_Present }}"
sound.fileName = "-1"
sound.autodetect = "TRUE"
tools.syncTime = "TRUE"
tools.upgrade.policy = "upgradeAtPowerCycle"
// USB
usb.pciSlotNumber = "32"
usb.present = "FALSE"
usb.present = "{{ .Usb_Present }}"
usb_xhci.present = "TRUE"
// Serial
serial0.present = "{{ .Serial_Present }}"
serial0.startConnected = "{{ .Serial_Present }}"
serial0.fileName = "{{ .Serial_Filename }}"
serial0.autodetect = "{{ .Serial_Auto }}"
serial0.fileType = "{{ .Serial_Type }}"
serial0.yieldOnMsrRead = "{{ .Serial_Yield }}"
serial0.pipe.endPoint = "{{ .Serial_Endpoint }}"
serial0.tryNoRxLoss = "{{ .Serial_Host }}"
// Parallel
parallel0.present = "{{ .Parallel_Present }}"
parallel0.startConnected = "{{ .Parallel_Present }}"
parallel0.fileName = "{{ .Parallel_Filename }}"
parallel0.autodetect = "{{ .Parallel_Auto }}"
parallel0.bidirectional = "{{ .Parallel_Bidirectional }}"
virtualHW.productCompatibility = "hosted"
virtualHW.version = "{{ .Version }}"
vmci0.id = "1861462627"

View File

@ -0,0 +1,409 @@
package iso
import (
"bytes"
"fmt"
"io/ioutil"
"math"
"math/rand"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"github.com/hashicorp/packer/packer"
"github.com/hashicorp/packer/provisioner/shell"
"github.com/hashicorp/packer/template"
"testing"
)
var vmxTestBuilderConfig = map[string]string{
"type": `"vmware-iso"`,
"iso_url": `"https://archive.org/download/ut-ttylinux-i686-12.6/ut-ttylinux-i686-12.6.iso"`,
"iso_checksum_type": `"md5"`,
"iso_checksum": `"43c1feeae55a44c6ef694b8eb18408a6"`,
"ssh_username": `"root"`,
"ssh_password": `"password"`,
"ssh_wait_timeout": `"45s"`,
"boot_command": `["<enter><wait5><wait10>","root<enter><wait>password<enter><wait>","udhcpc<enter><wait>"]`,
"shutdown_command": `"/sbin/shutdown -h; exit 0"`,
}
var vmxTestProvisionerConfig = map[string]string{
"type": `"shell"`,
"inline": `["echo hola mundo"]`,
}
const vmxTestTemplate string = `{"builders":[{%s}],"provisioners":[{%s}]}`
func tmpnam(prefix string) string {
var path string
var err error
const length = 16
dir := os.TempDir()
max := int(math.Pow(2, float64(length)))
n, err := rand.Intn(max), nil
for path = filepath.Join(dir, prefix+strconv.Itoa(n)); err == nil; _, err = os.Stat(path) {
n = rand.Intn(max)
path = filepath.Join(dir, prefix+strconv.Itoa(n))
}
return path
}
func createFloppyOutput(prefix string) (string, string, error) {
output := tmpnam(prefix)
f, err := os.Create(output)
if err != nil {
return "", "", fmt.Errorf("Unable to create empty %s: %s", output, err)
}
f.Close()
vmxData := []string{
`"floppy0.present":"TRUE"`,
`"floppy0.fileType":"file"`,
`"floppy0.clientDevice":"FALSE"`,
`"floppy0.fileName":"%s"`,
`"floppy0.startConnected":"TRUE"`,
}
outputFile := strings.Replace(output, "\\", "\\\\", -1)
vmxString := fmt.Sprintf("{"+strings.Join(vmxData, ",")+"}", outputFile)
return output, vmxString, nil
}
func readFloppyOutput(path string) (string, error) {
f, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("Unable to open file %s", path)
}
defer f.Close()
data, err := ioutil.ReadAll(f)
if err != nil {
return "", fmt.Errorf("Unable to read file: %s", err)
}
if len(data) == 0 {
return "", nil
}
return string(data[:bytes.IndexByte(data, 0)]), nil
}
func setupVMwareBuild(t *testing.T, builderConfig map[string]string, provisionerConfig map[string]string) error {
ui := packer.TestUi(t)
// create builder config and update with user-supplied options
cfgBuilder := map[string]string{}
for k, v := range vmxTestBuilderConfig {
cfgBuilder[k] = v
}
for k, v := range builderConfig {
cfgBuilder[k] = v
}
// convert our builder config into a single sprintfable string
builderLines := []string{}
for k, v := range cfgBuilder {
builderLines = append(builderLines, fmt.Sprintf(`"%s":%s`, k, v))
}
// create provisioner config and update with user-supplied options
cfgProvisioner := map[string]string{}
for k, v := range vmxTestProvisionerConfig {
cfgProvisioner[k] = v
}
for k, v := range provisionerConfig {
cfgProvisioner[k] = v
}
// convert our provisioner config into a single sprintfable string
provisionerLines := []string{}
for k, v := range cfgProvisioner {
provisionerLines = append(provisionerLines, fmt.Sprintf(`"%s":%s`, k, v))
}
// and now parse them into a template
configString := fmt.Sprintf(vmxTestTemplate, strings.Join(builderLines, `,`), strings.Join(provisionerLines, `,`))
tpl, err := template.Parse(strings.NewReader(configString))
if err != nil {
t.Fatalf("Unable to parse test config: %s", err)
}
// create our config to test the vmware-iso builder
components := packer.ComponentFinder{
Builder: func(n string) (packer.Builder, error) {
return &Builder{}, nil
},
Hook: func(n string) (packer.Hook, error) {
return &packer.DispatchHook{}, nil
},
PostProcessor: func(n string) (packer.PostProcessor, error) {
return &packer.MockPostProcessor{}, nil
},
Provisioner: func(n string) (packer.Provisioner, error) {
return &shell.Provisioner{}, nil
},
}
config := packer.CoreConfig{
Template: tpl,
Components: components,
}
// create a core using our template
core, err := packer.NewCore(&config)
if err != nil {
t.Fatalf("Unable to create core: %s", err)
}
// now we can prepare our build
b, err := core.Build("vmware-iso")
if err != nil {
t.Fatalf("Unable to create build: %s", err)
}
warn, err := b.Prepare()
if len(warn) > 0 {
for _, w := range warn {
t.Logf("Configuration warning: %s", w)
}
}
// and then finally build it
cache := &packer.FileCache{CacheDir: os.TempDir()}
artifacts, err := b.Run(ui, cache)
if err != nil {
t.Fatalf("Failed to build artifact: %s", err)
}
// check to see that we only got one artifact back
if len(artifacts) == 1 {
return artifacts[0].Destroy()
}
// otherwise some number of errors happened
t.Logf("Unexpected number of artifacts returned: %d", len(artifacts))
errors := make([]error, 0)
for _, artifact := range artifacts {
if err := artifact.Destroy(); err != nil {
errors = append(errors, err)
}
}
if len(errors) > 0 {
t.Errorf("%d Errors returned while trying to destroy artifacts", len(errors))
return fmt.Errorf("Error while trying to destroy artifacts: %v", errors)
}
return nil
}
func TestStepCreateVmx_SerialFile(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.")
}
tmpfile := tmpnam("SerialFileInput.")
serialConfig := map[string]string{
"serial": fmt.Sprintf(`"file:%s"`, filepath.ToSlash(tmpfile)),
}
error := setupVMwareBuild(t, serialConfig, map[string]string{})
if error != nil {
t.Errorf("Unable to read file: %s", error)
}
f, err := os.Stat(tmpfile)
if err != nil {
t.Errorf("VMware builder did not create a file for serial port: %s", err)
}
if f != nil {
if err := os.Remove(tmpfile); err != nil {
t.Fatalf("Unable to remove file %s: %s", tmpfile, err)
}
}
}
func TestStepCreateVmx_SerialPort(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.")
}
var defaultSerial string
if runtime.GOOS == "windows" {
defaultSerial = "COM1"
} else {
defaultSerial = "/dev/ttyS0"
}
config := map[string]string{
"serial": fmt.Sprintf(`"device:%s"`, filepath.ToSlash(defaultSerial)),
}
provision := map[string]string{
"inline": `"dmesg | egrep -o '^serial8250: ttyS1 at' > /dev/fd0"`,
}
// where to write output
output, vmxData, err := createFloppyOutput("SerialPortOutput.")
if err != nil {
t.Fatalf("Error creating output: %s", err)
}
defer func() {
if _, err := os.Stat(output); err == nil {
os.Remove(output)
}
}()
config["vmx_data"] = vmxData
t.Logf("Preparing to write output to %s", output)
// whee
err = setupVMwareBuild(t, config, provision)
if err != nil {
t.Errorf("%s", err)
}
// check the output
data, err := readFloppyOutput(output)
if err != nil {
t.Errorf("%s", err)
}
if data != "serial8250: ttyS1 at\n" {
t.Errorf("Serial port not detected : %v", data)
}
}
func TestStepCreateVmx_ParallelPort(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.")
}
var defaultParallel string
if runtime.GOOS == "windows" {
defaultParallel = "LPT1"
} else {
defaultParallel = "/dev/lp0"
}
config := map[string]string{
"parallel": fmt.Sprintf(`"device:%s,uni"`, filepath.ToSlash(defaultParallel)),
}
provision := map[string]string{
"inline": `"cat /proc/modules | egrep -o '^parport ' > /dev/fd0"`,
}
// where to write output
output, vmxData, err := createFloppyOutput("ParallelPortOutput.")
if err != nil {
t.Fatalf("Error creating output: %s", err)
}
defer func() {
if _, err := os.Stat(output); err == nil {
os.Remove(output)
}
}()
config["vmx_data"] = vmxData
t.Logf("Preparing to write output to %s", output)
// whee
error := setupVMwareBuild(t, config, provision)
if error != nil {
t.Errorf("%s", error)
}
// check the output
data, err := readFloppyOutput(output)
if err != nil {
t.Errorf("%s", err)
}
if data != "parport \n" {
t.Errorf("Parallel port not detected : %v", data)
}
}
func TestStepCreateVmx_Usb(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.")
}
config := map[string]string{
"usb": `"TRUE"`,
}
provision := map[string]string{
"inline": `"dmesg | egrep -m1 -o 'USB hub found$' > /dev/fd0"`,
}
// where to write output
output, vmxData, err := createFloppyOutput("UsbOutput.")
if err != nil {
t.Fatalf("Error creating output: %s", err)
}
defer func() {
if _, err := os.Stat(output); err == nil {
os.Remove(output)
}
}()
config["vmx_data"] = vmxData
t.Logf("Preparing to write output to %s", output)
// whee
error := setupVMwareBuild(t, config, provision)
if error != nil {
t.Errorf("%s", error)
}
// check the output
data, err := readFloppyOutput(output)
if err != nil {
t.Errorf("%s", err)
}
if data != "USB hub found\n" {
t.Errorf("USB support not detected : %v", data)
}
}
func TestStepCreateVmx_Sound(t *testing.T) {
if os.Getenv("PACKER_ACC") == "" {
t.Skip("This test is only run with PACKER_ACC=1 due to the requirement of access to the VMware binaries.")
}
config := map[string]string{
"sound": `"TRUE"`,
}
provision := map[string]string{
"inline": `"cat /proc/modules | egrep -o '^soundcore' > /dev/fd0"`,
}
// where to write output
output, vmxData, err := createFloppyOutput("SoundOutput.")
if err != nil {
t.Fatalf("Error creating output: %s", err)
}
defer func() {
if _, err := os.Stat(output); err == nil {
os.Remove(output)
}
}()
config["vmx_data"] = vmxData
t.Logf("Preparing to write output to %s", output)
// whee
error := setupVMwareBuild(t, config, provision)
if error != nil {
t.Errorf("Unable to read file: %s", error)
}
// check the output
data, err := readFloppyOutput(output)
if err != nil {
t.Errorf("%s", err)
}
if data != "soundcore\n" {
t.Errorf("Soundcard not detected : %v", data)
}
}

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