Merge remote-tracking branch 'upstream/master' into prestine

This commit is contained in:
Matthew Patton 2018-08-21 13:26:19 -04:00
commit e07491de59
354 changed files with 30953 additions and 8165 deletions

View file

@ -48,45 +48,42 @@ can quickly merge or address your contributions.
5. The issue is closed.
## Setting up Go to work on Packer
## Setting up Go
If you have never worked with Go before, you will have to complete the following
steps in order to be able to compile and test Packer. These instructions target
If you have never worked with Go before, you will have to install its
runtime in order to build packer.
1. [Install go](https://golang.org/doc/install#install)
## Setting up Packer for dev
If/when you have go installed you can already `go get` packer and `make` in
order to compile and test Packer. These instructions target
POSIX-like environments (Mac OS X, Linux, Cygwin, etc.) so you may need to
adjust them for Windows or other shells.
The instructions below are for go 1.7. or later.
1. [Download](https://golang.org/dl) and install Go. The instructions below are
for go 1.7. Earlier versions of Go are no longer supported.
2. Set and export the `GOPATH` environment variable and update your `PATH`. For
example, you can add the following to your `.bash_profile` (or comparable
shell startup scripts):
```
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin
```
3. Download the Packer source (and its dependencies) by running
1. Download the Packer source (and its dependencies) by running
`go get github.com/hashicorp/packer`. This will download the Packer source to
`$GOPATH/src/github.com/hashicorp/packer`.
4. When working on Packer, first `cd $GOPATH/src/github.com/hashicorp/packer`
2. When working on Packer, first `cd $GOPATH/src/github.com/hashicorp/packer`
so you can run `make` and easily access other files. Run `make help` to get
information about make targets.
5. Make your changes to the Packer source. You can run `make` in
3. Make your changes to the Packer source. You can run `make` in
`$GOPATH/src/github.com/hashicorp/packer` to run tests and build the Packer
binary. Any compilation errors will be shown when the binaries are
rebuilding. If you don't have `make` you can simply run
`go build -o bin/packer .` from the project root.
6. After running building Packer successfully, use
4. After running building Packer successfully, use
`$GOPATH/src/github.com/hashicorp/packer/bin/packer` to build a machine and
verify your changes work. For instance:
`$GOPATH/src/github.com/hashicorp/packer/bin/packer build template.json`.
7. If everything works well and the tests pass, run `go fmt` on your code before
5. If everything works well and the tests pass, run `go fmt` on your code before
submitting a pull-request.
### Opening an Pull Request

View file

@ -6,10 +6,9 @@ sudo: false
language: go
go:
- 1.8.x
- 1.9.x
- 1.x
- 1.10.x
- master
install:
- make deps
@ -22,4 +21,6 @@ branches:
- master
matrix:
allow_failures:
- go: master
fast_finish: true

View file

@ -1,3 +1,102 @@
## 1.2.6 (Unreleased)
### IMPROVEMENTS:
* builder/amazon-chroot: New feature `root_volume_tags` to tag the created
volumes. [GH-6504]
* builder/azure: Implement clean_image_name template engine. [GH-6558]
* builder/digitalocean: Add support for tagging to instances [GH-6546]
* builder/lxc: Allow unplivileged LXC containers. [GH-6279]
* builder/oci: Add `metadata` feature to Packer config. [GH-6498]
* builder/openstack: Add support for ports. [GH-6570]
* builder/openstack: Add support for getting config from clouds-public.yaml. [GH-6595]
* builder/openstack: Support Block Storage volumes as boot volume. [GH-6596]
* builder/openstack: Migrate floating IP usage to Network v2 API from Compute API. [GH-6373]
* builder/qemu: add ssh agent support. [GH-6541]
* builder/qemu: New `use_backing_file` feature [GH-6249]
* builder/vmware-iso: Try to use ISO files uploaded to the datastore when
building remotely instead of uploading them freshly every time [GH-5165]
* command/validate: Warn users if config needs fixing. [GH-6423]
* post-processor/vagrant: Support for Docker images. [GH-6494]
* postprocessor/vagrant: Add support for Azure. [GH-6576]
* provisioner/ansible: Enable {{.WinRMPassword}} template engine. [GH-6450]
* provisioner/shell-local: Create PACKER_HTTP_ADDR environment variable
[GH-6503]
### BUG FIXES:
* builder/amazon-ebssurrogate: Clean up volumes at end of build. [GH-6514]
* builder/azure: Generated password satisfies Azure password requirements
[GH-6480]
* builder/lxc: Correctly pass "config" option to "lxc launch". [GH-6563]
* builder/vmware-iso: Fix crash caused by invalid datacenter url. [GH-6529]
* core: Better error handling in downloader when connection error occurs.
[GH-6557]
* core: Fix broken pathing checks in checksum files. [GH-6525]
### BACKWARDS INCOMPATIBILITIES:
* builder/amazon: "owners" field on source_ami_filter is now required for
secuirty reasons. [GH-6585]
## 1.2.5 (July 16, 2018)
### BUG FIXES:
* builder/alickoud: Fix issue where internet_max_bandwidth_out template option
was not being passed to builder. [GH-6416]
* builder/alicloud: Fix an issue with VPC cleanup. [GH-6418]
* builder/amazon-chroot: Fix communicator bug that broke chroot builds.
[GH-6363]
* builder/amazon: Replace packer's waiters with those from the AWS sdk, solving
several timeout bugs. [GH-6332]
* builder/azure: update azure-sdk-for-go, fixing 32-bit build errors. [GH-6479]
* builder/azure: update the max length of managed_image_resource_group to match
new increased length of 90 characters. [GH-6477]
* builder/hyper-v: Fix secure boot template feature so that it properly passes
the temolate for MicrosoftUEFICertificateAuthority. [GH-6415]
* builder/hyperv: Fix bug in HyperV IP lookups that was causing breakages in
FreeBSD/OpenBSD builds. [GH-6416]
* builder/qemu: Fix error race condition in qemu builder that caused convert to
fail on ubuntu 18.x [GH-6437]
* builder/qemu: vnc_bind_address was not being passed to qemu. [GH-6467]
* builder/virtualbox: Allow iso_url to be a symlink. [GH-6370]
* builder/vmware: Don't fail on DHCP lease files that cannot be read, fixing
bug where builder failed on NAT networks that don't serve DHCP. [GH-6415]
* builder/vmware: Fix bug where we couldn't discover IP if vm_name differed
from the vmx displayName. [GH-6448]
* builder/vmware: Fix validation to prevent hang when remopte_password is not
sent but vmware is building on esxi. [GH-6424]
* builder/vmware:Correctly default the vm export format to ovf; this is what
the docs claimed we already did, but we didn't. [GH-4538]
* communicator/winrm: Revert an attempt to determine whether remote upload
destinations were files or directories, as this broke uploads on systems
without Powershell installed. [GH-6481]
* core: Fix bug in parsing of iso checksum files that arose when setting
iso_url to a relative filepath. [GH-6488]
* core: Fix Packer crash caused by improper error handling in the downloader.
[GH-6381]
* fix: Fix bug where fixer for ssh_private_ip that failed when boolean values
are passed as strings. [GH-6458]
* provisioner/powershell: Make upload of powershell variables retryable, in
case of system restarts. [GH-6388]
### IMPROVEMENTS:
* builder/amazon: Add the ap-northeast-3 region. [GH-6385]
* builder/amazon: Spot requests may now have tags applied using the `spot_tags`
option [GH-5452]
* builder/cloudstack: Add support for Projectid and new config option
prevent_firewall_changes. [GH-6487]
* builder/openstack: Add support for token authorization and cloud.yaml.
[GH-6362]
* builder/oracle-oci: Add new "instance_name" template option. [GH-6408]
* builder/scaleway: Add new "bootscript" parameter, allowing the user to not
use the default local bootscript [GH-6439]
* builder/vmware: Add support for linked clones to vmware-vmx. [GH-6394]
* debug: The -debug flag will now cause Packer to pause between provisioner
scripts in addition to Packer steps. [GH-4663]
* post-processor/googlecompute-import: Added new googlecompute-import post-
processor [GH-6451]
* provisioner/ansible: Add new "playbook_files" option to execute multiple
playbooks within one provisioner call. [GH-5086]
## 1.2.4 (May 29, 2018)
### BUG FIXES:
@ -52,6 +151,8 @@
* provisoner/shell-local: New options have been added to create feature parity
with the shell-local post-processor. This feature now works on Windows
hosts. [GH-5956]
* builder/virtualbox: Use HTTPS to download guest editions, now that it's
available. [GH-6406]
## 1.2.3 (April 25, 2018)

View file

@ -11,6 +11,8 @@ GOPATH=$(shell go env GOPATH)
# gofmt
UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l)
EXECUTABLE_FILES=$(shell find . -type f -perm +111 | egrep -v '^\./(vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats)' | egrep -v './provisioner/ansible/test-fixtures/exit1')
# Get the git commit
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
GIT_COMMIT=$(shell git rev-parse --short HEAD)
@ -43,9 +45,11 @@ package:
@sh -c "$(CURDIR)/scripts/dist.sh $(VERSION)"
deps:
@go get golang.org/x/tools/cmd/goimports
@go get golang.org/x/tools/cmd/stringer
@go get -u github.com/mna/pigeon
@go get github.com/kardianos/govendor
@go get golang.org/x/tools/cmd/goimports
@govendor sync
dev: deps ## Build and install a development build
@ -60,7 +64,7 @@ dev: deps ## Build and install a development build
@cp $(GOPATH)/bin/packer pkg/$(GOOS)_$(GOARCH)
fmt: ## Format Go code
@gofmt -w -s $(UNFORMATTED_FILES)
@gofmt -w -s main.go $(UNFORMATTED_FILES)
fmt-check: ## Check go code formatting
@echo "==> Checking that code complies with gofmt requirements..."
@ -73,6 +77,15 @@ fmt-check: ## Check go code formatting
echo "Check passed."; \
fi
mode-check: ## Check that only certain files are executable
@echo "==> Checking that only certain files are executable..."
@if [ ! -z "$(EXECUTABLE_FILES)" ]; then \
echo "These files should not be executable or they must be white listed in the Makefile:"; \
echo "$(EXECUTABLE_FILES)" | xargs -n1; \
exit 1; \
else \
echo "Check passed."; \
fi
fmt-docs:
@find ./website/source/docs -name "*.md" -exec pandoc --wrap auto --columns 79 --atx-headers -s -f "markdown_github+yaml_metadata_block" -t "markdown_github+yaml_metadata_block" {} -o {} \;
@ -88,7 +101,7 @@ generate: deps ## Generate dynamically generated code
goimports -w common/bootcommand/boot_command.go
gofmt -w command/plugin.go
test: deps fmt-check ## Run unit tests
test: deps fmt-check mode-check ## Run unit tests
@go test $(TEST) $(TESTARGS) -timeout=2m
@go tool vet $(VET) ; if [ $$? -eq 1 ]; then \
echo "ERROR: Vet found problems in the code."; \

4
Vagrantfile vendored
View file

@ -5,6 +5,10 @@ LINUX_BASE_BOX = "bento/ubuntu-16.04"
FREEBSD_BASE_BOX = "jen20/FreeBSD-12.0-CURRENT"
Vagrant.configure(2) do |config|
if Vagrant.has_plugin?("vagrant-cachier")
config.cache.scope = :box
end
# Compilation and development boxes
config.vm.define "linux", autostart: true, primary: true do |vmCfg|
vmCfg.vm.box = LINUX_BASE_BOX

View file

@ -140,6 +140,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
AssociatePublicIpAddress: b.config.AssociatePublicIpAddress,
RegionId: b.config.AlicloudRegion,
InternetChargeType: b.config.InternetChargeType,
InternetMaxBandwidthOut: b.config.InternetMaxBandwidthOut,
})
} else {
steps = append(steps, &stepConfigAlicloudPublicIP{

View file

@ -18,8 +18,12 @@ func (s *stepCheckAlicloudSourceImage) Run(_ context.Context, state multistep.St
client := state.Get("client").(*ecs.Client)
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
images, _, err := client.DescribeImages(&ecs.DescribeImagesArgs{RegionId: common.Region(config.AlicloudRegion),
ImageId: config.AlicloudSourceImage})
args := &ecs.DescribeImagesArgs{
RegionId: common.Region(config.AlicloudRegion),
ImageId: config.AlicloudSourceImage,
}
args.PageSize = 50
images, _, err := client.DescribeImages(args)
if err != nil {
err := fmt.Errorf("Error querying alicloud image: %s", err)
state.Put("error", err)
@ -27,6 +31,19 @@ func (s *stepCheckAlicloudSourceImage) Run(_ context.Context, state multistep.St
return multistep.ActionHalt
}
// Describe markerplace image
args.ImageOwnerAlias = ecs.ImageOwnerMarketplace
imageMarkets, _, err := client.DescribeImages(args)
if err != nil {
err := fmt.Errorf("Error querying alicloud marketplace image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(imageMarkets) > 0 {
images = append(images, imageMarkets...)
}
if len(images) == 0 {
err := fmt.Errorf("No alicloud image was found matching filters: %v", config.AlicloudSourceImage)
state.Put("error", err)

View file

@ -14,6 +14,7 @@ type stepConfigAlicloudEIP struct {
AssociatePublicIpAddress bool
RegionId string
InternetChargeType string
InternetMaxBandwidthOut int
allocatedId string
}
@ -24,6 +25,7 @@ func (s *stepConfigAlicloudEIP) Run(_ context.Context, state multistep.StateBag)
ui.Say("Allocating eip")
ipaddress, allocateId, err := client.AllocateEipAddress(&ecs.AllocateEipAddressArgs{
RegionId: common.Region(s.RegionId), InternetChargeType: common.InternetChargeType(s.InternetChargeType),
Bandwidth: s.InternetMaxBandwidthOut,
})
if err != nil {
state.Put("error", err)

View file

@ -85,7 +85,8 @@ func (s *stepConfigAlicloudVPC) Cleanup(state multistep.StateBag) {
e, _ := err.(*common.Error)
if (e.Code == "DependencyViolation.Instance" || e.Code == "DependencyViolation.RouteEntry" ||
e.Code == "DependencyViolation.VSwitch" ||
e.Code == "DependencyViolation.SecurityGroup") && time.Now().Before(timeoutPoint) {
e.Code == "DependencyViolation.SecurityGroup" ||
e.Code == "Forbbiden") && time.Now().Before(timeoutPoint) {
time.Sleep(1 * time.Second)
continue
}

View file

@ -44,6 +44,7 @@ type Config struct {
RootVolumeSize int64 `mapstructure:"root_volume_size"`
SourceAmi string `mapstructure:"source_ami"`
SourceAmiFilter awscommon.AmiFilterOptions `mapstructure:"source_ami_filter"`
RootVolumeTags awscommon.TagMap `mapstructure:"root_volume_tags"`
ctx interpolate.Context
}
@ -67,6 +68,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"ami_description",
"snapshot_tags",
"tags",
"root_volume_tags",
"command_wrapper",
"post_mount_commands",
"pre_mount_commands",
@ -230,6 +232,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
&StepPrepareDevice{},
&StepCreateVolume{
RootVolumeSize: b.config.RootVolumeSize,
RootVolumeTags: b.config.RootVolumeTags,
Ctx: b.config.ctx,
},
&StepAttachVolume{},
&StepEarlyUnflock{},

View file

@ -2,11 +2,10 @@ package chroot
import (
"context"
"errors"
"fmt"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
awscommon "github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/helper/multistep"
@ -24,7 +23,7 @@ type StepAttachVolume struct {
volumeId string
}
func (s *StepAttachVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepAttachVolume) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
device := state.Get("device").(string)
instance := state.Get("instance").(*ec2.Instance)
@ -52,35 +51,7 @@ func (s *StepAttachVolume) Run(_ context.Context, state multistep.StateBag) mult
s.volumeId = volumeId
// Wait for the volume to become attached
stateChange := awscommon.StateChangeConf{
Pending: []string{"attaching"},
StepState: state,
Target: "attached",
Refresh: func() (interface{}, string, error) {
attempts := 0
for attempts < 30 {
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIds: []*string{&volumeId}})
if err != nil {
return nil, "", err
}
if len(resp.Volumes[0].Attachments) > 0 {
a := resp.Volumes[0].Attachments[0]
return a, *a.State, nil
}
// When Attachment on volume is not present sleep for 2s and retry
attempts += 1
ui.Say(fmt.Sprintf(
"Volume %s show no attachments. Attempt %d/30. Sleeping for 2s and will retry.",
volumeId, attempts))
time.Sleep(2 * time.Second)
}
// Attachment on volume is not present after all attempts
return nil, "", errors.New("No attachments on volume.")
},
}
_, err = awscommon.WaitForState(&stateChange)
err = awscommon.WaitUntilVolumeAttached(ctx, ec2conn, s.volumeId)
if err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
@ -116,26 +87,7 @@ func (s *StepAttachVolume) CleanupFunc(state multistep.StateBag) error {
s.attached = false
// Wait for the volume to detach
stateChange := awscommon.StateChangeConf{
Pending: []string{"attaching", "attached", "detaching"},
StepState: state,
Target: "detached",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIds: []*string{&s.volumeId}})
if err != nil {
return nil, "", err
}
v := resp.Volumes[0]
if len(v.Attachments) > 0 {
return v, *v.Attachments[0].State, nil
} else {
return v, "detached", nil
}
},
}
_, err = awscommon.WaitForState(&stateChange)
err = awscommon.WaitUntilVolumeDetached(aws.BackgroundContext(), ec2conn, s.volumeId)
if err != nil {
return fmt.Errorf("Error waiting for volume: %s", err)
}

View file

@ -10,6 +10,7 @@ import (
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"
)
// StepCreateVolume creates a new volume from the snapshot of the root
@ -20,14 +21,36 @@ import (
type StepCreateVolume struct {
volumeId string
RootVolumeSize int64
RootVolumeTags awscommon.TagMap
Ctx interpolate.Context
}
func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepCreateVolume) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
ui := state.Get("ui").(packer.Ui)
volTags, err := s.RootVolumeTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
if err != nil {
err := fmt.Errorf("Error tagging volumes: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Collect tags for tagging on resource creation
var tagSpecs []*ec2.TagSpecification
if len(volTags) > 0 {
runVolTags := &ec2.TagSpecification{
ResourceType: aws.String("volume"),
Tags: volTags,
}
tagSpecs = append(tagSpecs, runVolTags)
}
var createVolume *ec2.CreateVolumeInput
if config.FromScratch {
createVolume = &ec2.CreateVolumeInput{
@ -69,6 +92,10 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
}
}
if len(tagSpecs) > 0 {
createVolume.SetTagSpecifications(tagSpecs)
volTags.Report(ui)
}
log.Printf("Create args: %+v", createVolume)
createVolumeResp, err := ec2conn.CreateVolume(createVolume)
@ -84,22 +111,7 @@ func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) mult
log.Printf("Volume ID: %s", s.volumeId)
// Wait for the volume to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"creating"},
StepState: state,
Target: "available",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.DescribeVolumes(&ec2.DescribeVolumesInput{VolumeIds: []*string{&s.volumeId}})
if err != nil {
return nil, "", err
}
v := resp.Volumes[0]
return v, *v.State, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
err = awscommon.WaitUntilVolumeAvailable(ctx, ec2conn, s.volumeId)
if err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)

View file

@ -18,7 +18,7 @@ type StepRegisterAMI struct {
EnableAMISriovNetSupport bool
}
func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
snapshotId := state.Get("snapshot_id").(string)
@ -102,16 +102,8 @@ func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multi
amis[*ec2conn.Config.Region] = *registerResp.ImageId
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *registerResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())

View file

@ -2,7 +2,6 @@ package chroot
import (
"context"
"errors"
"fmt"
"time"
@ -20,7 +19,7 @@ type StepSnapshot struct {
snapshotId string
}
func (s *StepSnapshot) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
volumeId := state.Get("volume_id").(string)
@ -44,26 +43,7 @@ func (s *StepSnapshot) Run(_ context.Context, state multistep.StateBag) multiste
ui.Message(fmt.Sprintf("Snapshot ID: %s", s.snapshotId))
// Wait for the snapshot to be ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
StepState: state,
Target: "completed",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{SnapshotIds: []*string{&s.snapshotId}})
if err != nil {
return nil, "", err
}
if len(resp.Snapshots) == 0 {
return nil, "", errors.New("No snapshots found.")
}
s := resp.Snapshots[0]
return s, *s.State, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
err = awscommon.WaitUntilSnapshotDone(ctx, ec2conn, s.snapshotId)
if err != nil {
err := fmt.Errorf("Error waiting for snapshot: %s", err)
state.Put("error", err)

View file

@ -25,6 +25,10 @@ func (d *AmiFilterOptions) Empty() bool {
return len(d.Owners) == 0 && len(d.Filters) == 0
}
func (d *AmiFilterOptions) NoOwner() bool {
return len(d.Owners) == 0
}
// RunConfig contains configuration for running an instance from a source
// AMI and details on how to access that launched image.
type RunConfig struct {
@ -43,6 +47,7 @@ type RunConfig struct {
SourceAmiFilter AmiFilterOptions `mapstructure:"source_ami_filter"`
SpotPrice string `mapstructure:"spot_price"`
SpotPriceAutoProduct string `mapstructure:"spot_price_auto_product"`
SpotTags map[string]string `mapstructure:"spot_tags"`
SubnetId string `mapstructure:"subnet_id"`
TemporaryKeyPairName string `mapstructure:"temporary_key_pair_name"`
TemporarySGSourceCidr string `mapstructure:"temporary_security_group_source_cidr"`
@ -100,6 +105,10 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
errs = append(errs, fmt.Errorf("A source_ami or source_ami_filter must be specified"))
}
if c.SourceAmi == "" && c.SourceAmiFilter.NoOwner() {
errs = append(errs, fmt.Errorf("For security reasons, your source AMI filter must declare an owner."))
}
if c.InstanceType == "" {
errs = append(errs, fmt.Errorf("An instance_type must be specified"))
}
@ -118,6 +127,13 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
if c.SpotTags != nil {
if c.SpotPrice == "" || c.SpotPrice == "0" {
errs = append(errs, fmt.Errorf(
"spot_tags should not be set when not requesting a spot instance"))
}
}
if c.UserData != "" && c.UserDataFile != "" {
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
} else if c.UserDataFile != "" {

View file

@ -55,18 +55,28 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) {
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
c := testConfig()
c.SourceAmi = ""
if err := c.Prepare(nil); len(err) != 1 {
if err := c.Prepare(nil); len(err) != 2 {
t.Fatalf("Should error if a source_ami (or source_ami_filter) is not specified")
}
}
func TestRunConfigPrepare_SourceAmiFilterBlank(t *testing.T) {
c := testConfigFilter()
if err := c.Prepare(nil); len(err) != 1 {
if err := c.Prepare(nil); len(err) != 2 {
t.Fatalf("Should error if source_ami_filter is empty or not specified (and source_ami is not specified)")
}
}
func TestRunConfigPrepare_SourceAmiFilterOwnersBlank(t *testing.T) {
c := testConfigFilter()
filter_key := "name"
filter_value := "foo"
c.SourceAmiFilter = AmiFilterOptions{Filters: map[*string]*string{&filter_key: &filter_value}}
if err := c.Prepare(nil); len(err) != 1 {
t.Fatalf("Should error if Owners is not specified)")
}
}
func TestRunConfigPrepare_SourceAmiFilterGood(t *testing.T) {
c := testConfigFilter()
owner := "123"

View file

@ -1,16 +1,13 @@
package common
import (
"errors"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/request"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer/helper/multistep"
)
@ -35,228 +32,304 @@ type StateChangeConf struct {
Target string
}
// AMIStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an AMI for state changes.
func AMIStateRefreshFunc(conn *ec2.EC2, imageId string) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeImages(&ec2.DescribeImagesInput{
ImageIds: []*string{&imageId},
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidAMIID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on AMIStateRefresh: %s", err)
return nil, "", err
}
}
// Following are wrapper functions that use Packer's environment-variables to
// determing retry logic, then call the AWS SDK's built-in waiters.
if resp == nil || len(resp.Images) == 0 {
// Sometimes AWS has consistency issues and doesn't see the
// AMI. Return an empty state.
return nil, "", nil
}
i := resp.Images[0]
return i, *i.State, nil
func WaitUntilAMIAvailable(ctx aws.Context, conn *ec2.EC2, imageId string) error {
imageInput := ec2.DescribeImagesInput{
ImageIds: []*string{&imageId},
}
err := conn.WaitUntilImageAvailableWithContext(
ctx,
&imageInput,
getWaiterOptions()...)
return err
}
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
// an EC2 instance.
func InstanceStateRefreshFunc(conn *ec2.EC2, instanceId string) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeInstances(&ec2.DescribeInstancesInput{
InstanceIds: []*string{&instanceId},
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidInstanceID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on InstanceStateRefresh: %s", err)
return nil, "", err
}
}
func WaitUntilInstanceTerminated(ctx aws.Context, conn *ec2.EC2, instanceId string) error {
if resp == nil || len(resp.Reservations) == 0 || len(resp.Reservations[0].Instances) == 0 {
// Sometimes AWS just has consistency issues and doesn't see
// our instance yet. Return an empty state.
return nil, "", nil
}
i := resp.Reservations[0].Instances[0]
return i, *i.State.Name, nil
instanceInput := ec2.DescribeInstancesInput{
InstanceIds: []*string{&instanceId},
}
err := conn.WaitUntilInstanceTerminatedWithContext(
ctx,
&instanceInput,
getWaiterOptions()...)
return err
}
// SpotRequestStateRefreshFunc returns a StateRefreshFunc that is used to watch
// a spot request for state changes.
func SpotRequestStateRefreshFunc(conn *ec2.EC2, spotRequestId string) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeSpotInstanceRequests(&ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIds: []*string{&spotRequestId},
})
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && ec2err.Code() == "InvalidSpotInstanceRequestID.NotFound" {
// Set this to nil as if we didn't find anything.
resp = nil
} else if isTransientNetworkError(err) {
// Transient network error, treat it as if we didn't find anything
resp = nil
} else {
log.Printf("Error on SpotRequestStateRefresh: %s", err)
return nil, "", err
}
}
if resp == nil || len(resp.SpotInstanceRequests) == 0 {
// Sometimes AWS has consistency issues and doesn't see the
// SpotRequest. Return an empty state.
return nil, "", nil
}
i := resp.SpotInstanceRequests[0]
return i, *i.State, nil
// This function works for both requesting and cancelling spot instances.
func WaitUntilSpotRequestFulfilled(ctx aws.Context, conn *ec2.EC2, spotRequestId string) error {
spotRequestInput := ec2.DescribeSpotInstanceRequestsInput{
SpotInstanceRequestIds: []*string{&spotRequestId},
}
err := conn.WaitUntilSpotInstanceRequestFulfilledWithContext(
ctx,
&spotRequestInput,
getWaiterOptions()...)
return err
}
func ImportImageRefreshFunc(conn *ec2.EC2, importTaskId string) StateRefreshFunc {
return func() (interface{}, string, error) {
resp, err := conn.DescribeImportImageTasks(&ec2.DescribeImportImageTasksInput{
ImportTaskIds: []*string{
&importTaskId,
func WaitUntilVolumeAvailable(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
volumeInput := ec2.DescribeVolumesInput{
VolumeIds: []*string{&volumeId},
}
err := conn.WaitUntilVolumeAvailableWithContext(
ctx,
&volumeInput,
getWaiterOptions()...)
return err
}
func WaitUntilSnapshotDone(ctx aws.Context, conn *ec2.EC2, snapshotID string) error {
snapInput := ec2.DescribeSnapshotsInput{
SnapshotIds: []*string{&snapshotID},
}
err := conn.WaitUntilSnapshotCompletedWithContext(
ctx,
&snapInput,
getWaiterOptions()...)
return err
}
// Wrappers for our custom AWS waiters
func WaitUntilVolumeAttached(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
volumeInput := ec2.DescribeVolumesInput{
VolumeIds: []*string{&volumeId},
}
err := WaitForVolumeToBeAttached(conn,
ctx,
&volumeInput,
getWaiterOptions()...)
return err
}
func WaitUntilVolumeDetached(ctx aws.Context, conn *ec2.EC2, volumeId string) error {
volumeInput := ec2.DescribeVolumesInput{
VolumeIds: []*string{&volumeId},
}
err := WaitForVolumeToBeDetached(conn,
ctx,
&volumeInput,
getWaiterOptions()...)
return err
}
func WaitUntilImageImported(ctx aws.Context, conn *ec2.EC2, taskID string) error {
importInput := ec2.DescribeImportImageTasksInput{
ImportTaskIds: []*string{&taskID},
}
err := WaitForImageToBeImported(conn,
ctx,
&importInput,
getWaiterOptions()...)
return err
}
// Custom waiters using AWS's request.Waiter
func WaitForVolumeToBeAttached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error {
w := request.Waiter{
Name: "DescribeVolumes",
MaxAttempts: 40,
Delay: request.ConstantWaiterDelay(5 * time.Second),
Acceptors: []request.WaiterAcceptor{
{
State: request.SuccessWaiterState,
Matcher: request.PathAllWaiterMatch,
Argument: "Volumes[].Attachments[].State",
Expected: "attached",
},
},
)
if err != nil {
if ec2err, ok := err.(awserr.Error); ok && strings.HasPrefix(ec2err.Code(), "InvalidConversionTaskId") {
resp = nil
} else if isTransientNetworkError(err) {
resp = nil
} else {
log.Printf("Error on ImportImageRefresh: %s", err)
return nil, "", err
Logger: c.Config.Logger,
NewRequest: func(opts []request.Option) (*request.Request, error) {
var inCpy *ec2.DescribeVolumesInput
if input != nil {
tmp := *input
inCpy = &tmp
}
}
if resp == nil || len(resp.ImportImageTasks) == 0 {
return nil, "", nil
}
i := resp.ImportImageTasks[0]
return i, *i.Status, nil
req, _ := c.DescribeVolumesRequest(inCpy)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return req, nil
},
}
return w.WaitWithContext(ctx)
}
// WaitForState watches an object and waits for it to achieve a certain
// state.
func WaitForState(conf *StateChangeConf) (i interface{}, err error) {
log.Printf("Waiting for state to become: %s", conf.Target)
sleepSeconds := SleepSeconds()
maxTicks := TimeoutSeconds()/sleepSeconds + 1
notfoundTick := 0
for {
var currentState string
i, currentState, err = conf.Refresh()
if err != nil {
return
}
if i == nil {
// If we didn't find the resource, check if we have been
// not finding it for awhile, and if so, report an error.
notfoundTick += 1
if notfoundTick > maxTicks {
return nil, errors.New("couldn't find resource")
func WaitForVolumeToBeDetached(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeVolumesInput, opts ...request.WaiterOption) error {
w := request.Waiter{
Name: "DescribeVolumes",
MaxAttempts: 40,
Delay: request.ConstantWaiterDelay(5 * time.Second),
Acceptors: []request.WaiterAcceptor{
{
State: request.SuccessWaiterState,
Matcher: request.PathAllWaiterMatch,
Argument: "length(Volumes[].Attachments[]) == `0`",
Expected: true,
},
},
Logger: c.Config.Logger,
NewRequest: func(opts []request.Option) (*request.Request, error) {
var inCpy *ec2.DescribeVolumesInput
if input != nil {
tmp := *input
inCpy = &tmp
}
} else {
// Reset the counter for when a resource isn't found
notfoundTick = 0
if currentState == conf.Target {
return
}
if conf.StepState != nil {
if _, ok := conf.StepState.GetOk(multistep.StateCancelled); ok {
return nil, errors.New("interrupted")
}
}
found := false
for _, allowed := range conf.Pending {
if currentState == allowed {
found = true
break
}
}
if !found {
err := fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
return nil, err
}
}
time.Sleep(time.Duration(sleepSeconds) * time.Second)
req, _ := c.DescribeVolumesRequest(inCpy)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return req, nil
},
}
return w.WaitWithContext(ctx)
}
func isTransientNetworkError(err error) bool {
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
return true
func WaitForImageToBeImported(c *ec2.EC2, ctx aws.Context, input *ec2.DescribeImportImageTasksInput, opts ...request.WaiterOption) error {
w := request.Waiter{
Name: "DescribeImages",
MaxAttempts: 300,
Delay: request.ConstantWaiterDelay(5 * time.Second),
Acceptors: []request.WaiterAcceptor{
{
State: request.SuccessWaiterState,
Matcher: request.PathAllWaiterMatch,
Argument: "ImportImageTasks[].Status",
Expected: "completed",
},
},
Logger: c.Config.Logger,
NewRequest: func(opts []request.Option) (*request.Request, error) {
var inCpy *ec2.DescribeImportImageTasksInput
if input != nil {
tmp := *input
inCpy = &tmp
}
req, _ := c.DescribeImportImageTasksRequest(inCpy)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return req, nil
},
}
return false
return w.WaitWithContext(ctx)
}
// Returns 300 seconds (5 minutes) by default
// Some AWS operations, like copying an AMI to a distant region, take a very long time
// Allow user to override with AWS_TIMEOUT_SECONDS environment variable
func TimeoutSeconds() (seconds int) {
seconds = 300
// This helper function uses the environment variables AWS_TIMEOUT_SECONDS and
// AWS_POLL_DELAY_SECONDS to generate waiter options that can be passed into any
// request.Waiter function. These options will control how many times the waiter
// will retry the request, as well as how long to wait between the retries.
override := os.Getenv("AWS_TIMEOUT_SECONDS")
// DEFAULTING BEHAVIOR:
// if AWS_POLL_DELAY_SECONDS is set but the others are not, Packer will set this
// poll delay and use the waiter-specific default
// if AWS_TIMEOUT_SECONDS is set but AWS_MAX_ATTEMPTS is not, Packer will use
// AWS_TIMEOUT_SECONDS and _either_ AWS_POLL_DELAY_SECONDS _or_ 2 if the user has not set AWS_POLL_DELAY_SECONDS, to determine a max number of attempts to make.
// if AWS_TIMEOUT_SECONDS, _and_ AWS_MAX_ATTEMPTS are both set,
// AWS_TIMEOUT_SECONDS will be ignored.
// if AWS_MAX_ATTEMPTS is set but AWS_POLL_DELAY_SECONDS is not, then we will
// use waiter-specific defaults.
type envInfo struct {
envKey string
Val int
overridden bool
}
type overridableWaitVars struct {
awsPollDelaySeconds envInfo
awsMaxAttempts envInfo
awsTimeoutSeconds envInfo
}
func getWaiterOptions() []request.WaiterOption {
envOverrides := getEnvOverrides()
waitOpts := applyEnvOverrides(envOverrides)
return waitOpts
}
func getOverride(varInfo envInfo) envInfo {
override := os.Getenv(varInfo.envKey)
if override != "" {
n, err := strconv.Atoi(override)
if err != nil {
log.Printf("Invalid timeout seconds '%s', using default", override)
log.Printf("Invalid %s '%s', using default", varInfo.envKey, override)
} else {
seconds = n
varInfo.overridden = true
varInfo.Val = n
}
}
log.Printf("Allowing %ds to complete (change with AWS_TIMEOUT_SECONDS)", seconds)
return seconds
return varInfo
}
// Returns 2 seconds by default
// AWS async operations sometimes takes long times, if there are multiple parallel builds,
// polling at 2 second frequency will exceed the request limit. Allow 2 seconds to be
// overwritten with AWS_POLL_DELAY_SECONDS
func SleepSeconds() (seconds int) {
seconds = 2
override := os.Getenv("AWS_POLL_DELAY_SECONDS")
if override != "" {
n, err := strconv.Atoi(override)
if err != nil {
log.Printf("Invalid sleep seconds '%s', using default", override)
} else {
seconds = n
}
func getEnvOverrides() overridableWaitVars {
// Load env vars from environment, and use them to override defaults
envValues := overridableWaitVars{
envInfo{"AWS_POLL_DELAY_SECONDS", 2, false},
envInfo{"AWS_MAX_ATTEMPTS", 0, false},
envInfo{"AWS_TIMEOUT_SECONDS", 300, false},
}
log.Printf("Using %ds as polling delay (change with AWS_POLL_DELAY_SECONDS)", seconds)
return seconds
envValues.awsMaxAttempts = getOverride(envValues.awsMaxAttempts)
envValues.awsPollDelaySeconds = getOverride(envValues.awsPollDelaySeconds)
envValues.awsTimeoutSeconds = getOverride(envValues.awsTimeoutSeconds)
return envValues
}
func applyEnvOverrides(envOverrides overridableWaitVars) []request.WaiterOption {
waitOpts := make([]request.WaiterOption, 0)
// If user has set poll delay seconds, overwrite it. If user has NOT,
// default to a poll delay of 2 seconds
if envOverrides.awsPollDelaySeconds.overridden {
delaySeconds := request.ConstantWaiterDelay(time.Duration(envOverrides.awsPollDelaySeconds.Val) * time.Second)
waitOpts = append(waitOpts, request.WithWaiterDelay(delaySeconds))
}
// If user has set max attempts, overwrite it. If user hasn't set max
// attempts, default to whatever the waiter has set as a default.
if envOverrides.awsMaxAttempts.overridden {
waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(envOverrides.awsMaxAttempts.Val))
}
if envOverrides.awsMaxAttempts.overridden && envOverrides.awsTimeoutSeconds.overridden {
log.Printf("WARNING: AWS_MAX_ATTEMPTS and AWS_TIMEOUT_SECONDS are" +
" both set. Packer will be using AWS_MAX_ATTEMPTS and discarding " +
"AWS_TIMEOUT_SECONDS. If you have not set AWS_POLL_DELAY_SECONDS, " +
"Packer will default to a 2 second poll delay.")
} else if envOverrides.awsTimeoutSeconds.overridden {
log.Printf("DEPRECATION WARNING: env var AWS_TIMEOUT_SECONDS is " +
"deprecated in favor of AWS_MAX_ATTEMPTS. If you have not " +
"explicitly set AWS_POLL_DELAY_SECONDS, we are defaulting to a " +
"poll delay of 2 seconds, regardless of the AWS waiter's default.")
maxAttempts := envOverrides.awsTimeoutSeconds.Val / envOverrides.awsPollDelaySeconds.Val
// override the delay so we can get the timeout right
if !envOverrides.awsPollDelaySeconds.overridden {
delaySeconds := request.ConstantWaiterDelay(time.Duration(envOverrides.awsPollDelaySeconds.Val) * time.Second)
waitOpts = append(waitOpts, request.WithWaiterDelay(delaySeconds))
}
waitOpts = append(waitOpts, request.WithWaiterMaxAttempts(maxAttempts))
}
if len(waitOpts) == 0 {
log.Printf("No AWS timeout and polling overrides have been set. " +
"Packer will default to waiter-specific delays and timeouts. If you would " +
"like to customize the length of time between retries and max " +
"number of retries you may do so by setting the environment " +
"variables AWS_POLL_DELAY_SECONDS and AWS_MAX_ATTEMPTS to your " +
"desired values.")
}
return waitOpts
}

View file

@ -0,0 +1,66 @@
package common
import (
"reflect"
"testing"
"time"
"github.com/aws/aws-sdk-go/aws/request"
)
func testGetWaiterOptions(t *testing.T) {
// no vars are set
envValues := overridableWaitVars{
envInfo{"AWS_POLL_DELAY_SECONDS", 2, false},
envInfo{"AWS_MAX_ATTEMPTS", 0, false},
envInfo{"AWS_TIMEOUT_SECONDS", 300, false},
}
options := applyEnvOverrides(envValues)
if len(options) > 0 {
t.Fatalf("Did not expect any waiter options to be generated; actual: %#v", options)
}
// all vars are set
envValues = overridableWaitVars{
envInfo{"AWS_POLL_DELAY_SECONDS", 1, true},
envInfo{"AWS_MAX_ATTEMPTS", 800, true},
envInfo{"AWS_TIMEOUT_SECONDS", 20, true},
}
options = applyEnvOverrides(envValues)
expected := []request.WaiterOption{
request.WithWaiterDelay(request.ConstantWaiterDelay(time.Duration(1) * time.Second)),
request.WithWaiterMaxAttempts(800),
}
if !reflect.DeepEqual(options, expected) {
t.Fatalf("expected != actual!! Expected: %#v; Actual: %#v.", expected, options)
}
// poll delay is not set
envValues = overridableWaitVars{
envInfo{"AWS_POLL_DELAY_SECONDS", 2, false},
envInfo{"AWS_MAX_ATTEMPTS", 800, true},
envInfo{"AWS_TIMEOUT_SECONDS", 300, false},
}
options = applyEnvOverrides(envValues)
expected = []request.WaiterOption{
request.WithWaiterMaxAttempts(800),
}
if !reflect.DeepEqual(options, expected) {
t.Fatalf("expected != actual!! Expected: %#v; Actual: %#v.", expected, options)
}
// poll delay is not set but timeout seconds is
envValues = overridableWaitVars{
envInfo{"AWS_POLL_DELAY_SECONDS", 2, false},
envInfo{"AWS_MAX_ATTEMPTS", 0, false},
envInfo{"AWS_TIMEOUT_SECONDS", 20, true},
}
options = applyEnvOverrides(envValues)
expected = []request.WaiterOption{
request.WithWaiterDelay(request.ConstantWaiterDelay(time.Duration(2) * time.Second)),
request.WithWaiterMaxAttempts(10),
}
if !reflect.DeepEqual(options, expected) {
t.Fatalf("expected != actual!! Expected: %#v; Actual: %#v.", expected, options)
}
}

View file

@ -20,7 +20,7 @@ type StepAMIRegionCopy struct {
Name string
}
func (s *StepAMIRegionCopy) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepAMIRegionCopy) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
amis := state.Get("amis").(map[string]string)
@ -53,7 +53,7 @@ func (s *StepAMIRegionCopy) Run(_ context.Context, state multistep.StateBag) mul
go func(region string) {
defer wg.Done()
id, snapshotIds, err := amiRegionCopy(state, s.AccessConfig, s.Name, ami, region, *ec2conn.Config.Region, regKeyID)
id, snapshotIds, err := amiRegionCopy(ctx, state, s.AccessConfig, s.Name, ami, region, *ec2conn.Config.Region, regKeyID)
lock.Lock()
defer lock.Unlock()
amis[region] = id
@ -85,7 +85,7 @@ func (s *StepAMIRegionCopy) Cleanup(state multistep.StateBag) {
// amiRegionCopy does a copy for the given AMI to the target region and
// returns the resulting ID and snapshot IDs, or error.
func amiRegionCopy(state multistep.StateBag, config *AccessConfig, name string, imageId string,
func amiRegionCopy(ctx context.Context, state multistep.StateBag, config *AccessConfig, name string, imageId string,
target string, source string, keyID string) (string, []string, error) {
snapshotIds := []string{}
isEncrypted := false
@ -116,14 +116,8 @@ func amiRegionCopy(state multistep.StateBag, config *AccessConfig, name string,
imageId, target, err)
}
stateChange := StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: AMIStateRefreshFunc(regionconn, *resp.ImageId),
StepState: state,
}
if _, err := WaitForState(&stateChange); err != nil {
// Wait for the image to become ready
if err := WaitUntilAMIAvailable(ctx, regionconn, *resp.ImageId); err != nil {
return "", snapshotIds, fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s",
*resp.ImageId, target, err)
}

View file

@ -1,4 +1,4 @@
package ebs
package common
import (
"context"
@ -6,7 +6,6 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/hashicorp/packer/builder/amazon/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
@ -14,16 +13,16 @@ import (
// stepCleanupVolumes cleans up any orphaned volumes that were not designated to
// remain after termination of the instance. These volumes are typically ones
// that are marked as "delete on terminate:false" in the source_ami of a build.
type stepCleanupVolumes struct {
BlockDevices common.BlockDevices
type StepCleanupVolumes struct {
BlockDevices BlockDevices
}
func (s *stepCleanupVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepCleanupVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// stepCleanupVolumes is for Cleanup only
return multistep.ActionContinue
}
func (s *stepCleanupVolumes) Cleanup(state multistep.StateBag) {
func (s *StepCleanupVolumes) Cleanup(state multistep.StateBag) {
ec2conn := state.Get("ec2").(*ec2.EC2)
instanceRaw := state.Get("instance")
var instance *ec2.Instance

View file

@ -19,7 +19,7 @@ type StepCreateEncryptedAMICopy struct {
AMIMappings []BlockDevice
}
func (s *StepCreateEncryptedAMICopy) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepCreateEncryptedAMICopy) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
kmsKeyId := s.KeyID
@ -65,15 +65,8 @@ func (s *StepCreateEncryptedAMICopy) Run(_ context.Context, state multistep.Stat
}
// Wait for the copy to become ready
stateChange := StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: AMIStateRefreshFunc(ec2conn, *copyResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI copy to become ready...")
if _, err := WaitForState(&stateChange); err != nil {
if err := WaitUntilAMIAvailable(ctx, ec2conn, *copyResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI Copy: %s", err)
state.Put("error", err)
ui.Error(err.Error())

View file

@ -303,14 +303,8 @@ func (s *StepRunSourceInstance) Cleanup(state multistep.StateBag) {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instanceId),
Target: "terminated",
}
_, err := WaitForState(&stateChange)
if err != nil {
if err := WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
ui.Error(err.Error())
}
}

View file

@ -32,6 +32,7 @@ type StepRunSpotInstance struct {
SourceAMI string
SpotPrice string
SpotPriceProduct string
SpotTags TagMap
SubnetId string
Tags TagMap
VolumeTags TagMap
@ -202,13 +203,7 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
spotRequestId := s.spotRequest.SpotInstanceRequestId
ui.Message(fmt.Sprintf("Waiting for spot request (%s) to become active...", *spotRequestId))
stateChange := StateChangeConf{
Pending: []string{"open"},
Target: "active",
Refresh: SpotRequestStateRefreshFunc(ec2conn, *spotRequestId),
StepState: state,
}
_, err = WaitForState(&stateChange)
err = WaitUntilSpotRequestFulfilled(ctx, ec2conn, *spotRequestId)
if err != nil {
err := fmt.Errorf("Error waiting for spot request (%s) to become ready: %s", *spotRequestId, err)
state.Put("error", err)
@ -227,6 +222,33 @@ func (s *StepRunSpotInstance) Run(ctx context.Context, state multistep.StateBag)
}
instanceId = *spotResp.SpotInstanceRequests[0].InstanceId
// Tag spot instance request
spotTags, err := s.SpotTags.EC2Tags(s.Ctx, *ec2conn.Config.Region, state)
if err != nil {
err := fmt.Errorf("Error tagging spot request: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
spotTags.Report(ui)
if len(spotTags) > 0 && s.SpotTags.IsSet() {
// 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: spotTags,
Resources: []*string{spotRequestId},
})
return true, err
})
if err != nil {
err := fmt.Errorf("Error tagging spot request: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Set the instance ID so that the cleanup works properly
s.instanceId = instanceId
@ -344,13 +366,8 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
ui.Error(fmt.Sprintf("Error cancelling the spot request, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"active", "open"},
Refresh: SpotRequestStateRefreshFunc(ec2conn, *s.spotRequest.SpotInstanceRequestId),
Target: "cancelled",
}
_, err := WaitForState(&stateChange)
err := WaitUntilSpotRequestFulfilled(aws.BackgroundContext(), ec2conn, *s.spotRequest.SpotInstanceRequestId)
if err != nil {
ui.Error(err.Error())
}
@ -364,14 +381,8 @@ func (s *StepRunSpotInstance) Cleanup(state multistep.StateBag) {
ui.Error(fmt.Sprintf("Error terminating instance, may still be around: %s", err))
return
}
stateChange := StateChangeConf{
Pending: []string{"pending", "running", "shutting-down", "stopped", "stopping"},
Refresh: InstanceStateRefreshFunc(ec2conn, s.instanceId),
Target: "terminated",
}
_, err := WaitForState(&stateChange)
if err != nil {
if err := WaitUntilInstanceTerminated(aws.BackgroundContext(), ec2conn, s.instanceId); err != nil {
ui.Error(err.Error())
}
}

View file

@ -78,9 +78,11 @@ func (s *StepStopEBSBackedInstance) Run(ctx context.Context, state multistep.Sta
// Wait for the instance to actually stop
ui.Say("Waiting for the instance to stop...")
err = ec2conn.WaitUntilInstanceStoppedWithContext(ctx, &ec2.DescribeInstancesInput{
InstanceIds: []*string{instance.InstanceId},
})
err = ec2conn.WaitUntilInstanceStoppedWithContext(ctx,
&ec2.DescribeInstancesInput{
InstanceIds: []*string{instance.InstanceId},
},
getWaiterOptions()...)
if err != nil {
err := fmt.Errorf("Error waiting for instance to stop: %s", err)

View file

@ -48,6 +48,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"ami_description",
"run_tags",
"run_volume_tags",
"spot_tags",
"snapshot_tags",
"tags",
},
@ -134,6 +135,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SourceAMI: b.config.SourceAmi,
SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
SpotTags: b.config.SpotTags,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,
UserData: b.config.UserData,
@ -189,7 +191,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VpcId: b.config.VpcId,
TemporarySGSourceCidr: b.config.TemporarySGSourceCidr,
},
&stepCleanupVolumes{
&awscommon.StepCleanupVolumes{
BlockDevices: b.config.BlockDevices,
},
instanceStep,

View file

@ -15,7 +15,7 @@ type stepCreateAMI struct {
image *ec2.Image
}
func (s *stepCreateAMI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *stepCreateAMI) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
instance := state.Get("instance").(*ec2.Instance)
@ -44,15 +44,8 @@ func (s *stepCreateAMI) Run(_ context.Context, state multistep.StateBag) multist
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *createResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *createResp.ImageId); err != nil {
log.Printf("Error waiting for AMI: %s", err)
imagesResp, err := ec2conn.DescribeImages(&ec2.DescribeImagesInput{ImageIds: []*string{createResp.ImageId}})
if err != nil {

View file

@ -48,6 +48,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"run_tags",
"run_volume_tags",
"snapshot_tags",
"spot_tags",
"tags",
},
},
@ -148,6 +149,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SourceAMI: b.config.SourceAmi,
SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
SpotTags: b.config.SpotTags,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,
UserData: b.config.UserData,
@ -206,6 +208,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
VpcId: b.config.VpcId,
TemporarySGSourceCidr: b.config.TemporarySGSourceCidr,
},
&awscommon.StepCleanupVolumes{
BlockDevices: b.config.BlockDevices,
},
instanceStep,
&awscommon.StepGetPassword{
Debug: b.config.PackerDebug,

View file

@ -21,7 +21,7 @@ type StepRegisterAMI struct {
image *ec2.Image
}
func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
snapshotIds := state.Get("snapshot_ids").(map[string]string)
@ -63,15 +63,8 @@ func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multi
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *registerResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())

View file

@ -2,7 +2,6 @@ package ebssurrogate
import (
"context"
"errors"
"fmt"
"sync"
"time"
@ -23,7 +22,7 @@ type StepSnapshotVolumes struct {
snapshotIds map[string]string
}
func (s *StepSnapshotVolumes) snapshotVolume(deviceName string, state multistep.StateBag) error {
func (s *StepSnapshotVolumes) snapshotVolume(ctx context.Context, deviceName string, state multistep.StateBag) error {
ec2conn := state.Get("ec2").(*ec2.EC2)
ui := state.Get("ui").(packer.Ui)
instance := state.Get("instance").(*ec2.Instance)
@ -52,33 +51,12 @@ func (s *StepSnapshotVolumes) snapshotVolume(deviceName string, state multistep.
// Set the snapshot ID so we can delete it later
s.snapshotIds[deviceName] = *createSnapResp.SnapshotId
// Wait for the snapshot to be ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
StepState: state,
Target: "completed",
Refresh: func() (interface{}, string, error) {
resp, err := ec2conn.DescribeSnapshots(&ec2.DescribeSnapshotsInput{
SnapshotIds: []*string{createSnapResp.SnapshotId},
})
if err != nil {
return nil, "", err
}
if len(resp.Snapshots) == 0 {
return nil, "", errors.New("No snapshots found.")
}
s := resp.Snapshots[0]
return s, *s.State, nil
},
}
_, err = awscommon.WaitForState(&stateChange)
// Wait for snapshot to be created
err = awscommon.WaitUntilSnapshotDone(ctx, ec2conn, *createSnapResp.SnapshotId)
return err
}
func (s *StepSnapshotVolumes) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepSnapshotVolumes) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
s.snapshotIds = map[string]string{}
@ -89,7 +67,7 @@ func (s *StepSnapshotVolumes) Run(_ context.Context, state multistep.StateBag) m
wg.Add(1)
go func(device *ec2.BlockDeviceMapping) {
defer wg.Done()
if err := s.snapshotVolume(*device.DeviceName, state); err != nil {
if err := s.snapshotVolume(ctx, *device.DeviceName, state); err != nil {
errs = multierror.Append(errs, err)
}
}(device)

View file

@ -44,6 +44,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_tags",
"spot_tags",
"ebs_volumes",
},
},
@ -132,6 +133,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SourceAMI: b.config.SourceAmi,
SpotPrice: b.config.SpotPrice,
SpotPriceProduct: b.config.SpotPriceAutoProduct,
SpotTags: b.config.SpotTags,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,
UserData: b.config.UserData,

View file

@ -69,6 +69,7 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
"run_volume_tags",
"snapshot_tags",
"tags",
"spot_tags",
},
},
}, configs...)
@ -219,6 +220,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
SpotPriceProduct: b.config.SpotPriceAutoProduct,
SubnetId: b.config.SubnetId,
Tags: b.config.RunTags,
SpotTags: b.config.SpotTags,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
}

View file

@ -16,7 +16,7 @@ type StepRegisterAMI struct {
EnableAMISriovNetSupport bool
}
func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *StepRegisterAMI) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(*Config)
ec2conn := state.Get("ec2").(*ec2.EC2)
manifestPath := state.Get("remote_manifest_path").(string)
@ -58,15 +58,8 @@ func (s *StepRegisterAMI) Run(_ context.Context, state multistep.StateBag) multi
state.Put("amis", amis)
// Wait for the image to become ready
stateChange := awscommon.StateChangeConf{
Pending: []string{"pending"},
Target: "available",
Refresh: awscommon.AMIStateRefreshFunc(ec2conn, *registerResp.ImageId),
StepState: state,
}
ui.Say("Waiting for AMI to become ready...")
if _, err := awscommon.WaitForState(&stateChange); err != nil {
if err := awscommon.WaitUntilAMIAvailable(ctx, ec2conn, *registerResp.ImageId); err != nil {
err := fmt.Errorf("Error waiting for AMI: %s", err)
state.Put("error", err)
ui.Error(err.Error())

View file

@ -18,6 +18,9 @@ type AdditionalDiskArtifact struct {
}
type Artifact struct {
// OS type: Linux, Windows
OSType string
// VHD
StorageAccountLocation string
OSDiskUri string
@ -29,20 +32,23 @@ type Artifact struct {
ManagedImageResourceGroupName string
ManagedImageName string
ManagedImageLocation string
ManagedImageId string
// Additional Disks
AdditionalDisks *[]AdditionalDiskArtifact
}
func NewManagedImageArtifact(resourceGroup, name, location string) (*Artifact, error) {
func NewManagedImageArtifact(osType, resourceGroup, name, location, id string) (*Artifact, error) {
return &Artifact{
ManagedImageResourceGroupName: resourceGroup,
ManagedImageName: name,
ManagedImageLocation: location,
ManagedImageId: id,
OSType: osType,
}, nil
}
func NewArtifact(template *CaptureTemplate, getSasUrl func(name string) string) (*Artifact, error) {
func NewArtifact(template *CaptureTemplate, getSasUrl func(name string) string, osType string) (*Artifact, error) {
if template == nil {
return nil, fmt.Errorf("nil capture template")
}
@ -76,6 +82,7 @@ func NewArtifact(template *CaptureTemplate, getSasUrl func(name string) string)
}
return &Artifact{
OSType: osType,
OSDiskUri: vhdUri.String(),
OSDiskUriReadOnlySas: getSasUrl(getStorageUrlPath(vhdUri)),
TemplateUri: templateUri.String(),
@ -142,9 +149,11 @@ func (a *Artifact) String() string {
var buf bytes.Buffer
buf.WriteString(fmt.Sprintf("%s:\n\n", a.BuilderId()))
buf.WriteString(fmt.Sprintf("OSType: %s\n", a.OSType))
if a.isManagedImage() {
buf.WriteString(fmt.Sprintf("ManagedImageResourceGroupName: %s\n", a.ManagedImageResourceGroupName))
buf.WriteString(fmt.Sprintf("ManagedImageName: %s\n", a.ManagedImageName))
buf.WriteString(fmt.Sprintf("ManagedImageId: %s\n", a.ManagedImageId))
buf.WriteString(fmt.Sprintf("ManagedImageLocation: %s\n", a.ManagedImageLocation))
} else {
buf.WriteString(fmt.Sprintf("StorageAccountLocation: %s\n", a.StorageAccountLocation))

View file

@ -28,7 +28,7 @@ func TestArtifactId(t *testing.T) {
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl)
artifact, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err != nil {
t.Fatalf("err=%s", err)
}
@ -59,7 +59,7 @@ func TestArtifactString(t *testing.T) {
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl)
artifact, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err != nil {
t.Fatalf("err=%s", err)
}
@ -80,6 +80,9 @@ func TestArtifactString(t *testing.T) {
if !strings.Contains(testSubject, "StorageAccountLocation: southcentralus") {
t.Errorf("Expected String() output to contain StorageAccountLocation")
}
if !strings.Contains(testSubject, "OSType: Linux") {
t.Errorf("Expected String() output to contain OSType")
}
}
func TestAdditionalDiskArtifactString(t *testing.T) {
@ -107,7 +110,7 @@ func TestAdditionalDiskArtifactString(t *testing.T) {
},
}
artifact, err := NewArtifact(&template, getFakeSasUrl)
artifact, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err != nil {
t.Fatalf("err=%s", err)
}
@ -128,6 +131,9 @@ func TestAdditionalDiskArtifactString(t *testing.T) {
if !strings.Contains(testSubject, "StorageAccountLocation: southcentralus") {
t.Errorf("Expected String() output to contain StorageAccountLocation")
}
if !strings.Contains(testSubject, "OSType: Linux") {
t.Errorf("Expected String() output to contain OSType")
}
if !strings.Contains(testSubject, "AdditionalDiskUri (datadisk-1): https://storage.blob.core.windows.net/system/Microsoft.Compute/Images/images/packer-datadisk-1.4085bb15-3644-4641-b9cd-f575918640b4.vhd") {
t.Errorf("Expected String() output to contain AdditionalDiskUri")
}
@ -154,7 +160,7 @@ func TestArtifactProperties(t *testing.T) {
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl)
testSubject, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err != nil {
t.Fatalf("err=%s", err)
}
@ -174,6 +180,9 @@ func TestArtifactProperties(t *testing.T) {
if testSubject.StorageAccountLocation != "southcentralus" {
t.Errorf("Expected StorageAccountLocation to be 'southcentral', but got %s", testSubject.StorageAccountLocation)
}
if testSubject.OSType != "Linux" {
t.Errorf("Expected OSType to be 'Linux', but got %s", testSubject.OSType)
}
}
func TestAdditionalDiskArtifactProperties(t *testing.T) {
@ -201,7 +210,7 @@ func TestAdditionalDiskArtifactProperties(t *testing.T) {
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl)
testSubject, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err != nil {
t.Fatalf("err=%s", err)
}
@ -221,6 +230,9 @@ func TestAdditionalDiskArtifactProperties(t *testing.T) {
if testSubject.StorageAccountLocation != "southcentralus" {
t.Errorf("Expected StorageAccountLocation to be 'southcentral', but got %s", testSubject.StorageAccountLocation)
}
if testSubject.OSType != "Linux" {
t.Errorf("Expected OSType to be 'Linux', but got %s", testSubject.OSType)
}
if testSubject.AdditionalDisks == nil {
t.Errorf("Expected AdditionalDisks to be not nil")
}
@ -253,7 +265,7 @@ func TestArtifactOverHyphenatedCaptureUri(t *testing.T) {
},
}
testSubject, err := NewArtifact(&template, getFakeSasUrl)
testSubject, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err != nil {
t.Fatalf("err=%s", err)
}
@ -266,7 +278,7 @@ func TestArtifactOverHyphenatedCaptureUri(t *testing.T) {
func TestArtifactRejectMalformedTemplates(t *testing.T) {
template := CaptureTemplate{}
_, err := NewArtifact(&template, getFakeSasUrl)
_, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err == nil {
t.Fatalf("Expected artifact creation to fail, but it succeeded.")
}
@ -289,7 +301,7 @@ func TestArtifactRejectMalformedStorageUri(t *testing.T) {
},
}
_, err := NewArtifact(&template, getFakeSasUrl)
_, err := NewArtifact(&template, getFakeSasUrl, "Linux")
if err == nil {
t.Fatalf("Expected artifact creation to fail, but it succeeded.")
}

View file

@ -255,7 +255,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
if b.config.isManagedImage() {
return NewManagedImageArtifact(b.config.ManagedImageResourceGroupName, b.config.ManagedImageName, b.config.manageImageLocation)
managedImageID := fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/Microsoft.Compute/images/%s", b.config.SubscriptionID, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName)
return NewManagedImageArtifact(b.config.OSType, b.config.ManagedImageResourceGroupName, b.config.ManagedImageName, b.config.manageImageLocation, managedImageID)
} else if template, ok := b.stateBag.GetOk(constants.ArmCaptureTemplate); ok {
return NewArtifact(
template.(*CaptureTemplate),
@ -266,7 +267,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
options.Expiry = time.Now().AddDate(0, 1, 0).UTC() // one month
sasUrl, _ := blob.GetSASURI(options)
return sasUrl
})
},
b.config.OSType)
}
return &Artifact{}, nil

View file

@ -47,7 +47,7 @@ const (
// -> ^[^_\W][\w-._]{0,79}(?<![-.])$
//
// This is not an exhaustive match, but it should be extremely close.
validResourceGroupNameRe = "^[^_\\W][\\w-._\\(\\)]{0,63}$"
validResourceGroupNameRe = "^[^_\\W][\\w-._\\(\\)]{0,89}$"
validManagedDiskName = "^[^_\\W][\\w-._)]{0,79}$"
)
@ -150,7 +150,7 @@ type Config struct {
winrmCertificate string
Comm communicator.Config `mapstructure:",squash"`
ctx *interpolate.Context
ctx interpolate.Context
//Cleanup
AsyncResourceGroupDelete bool `mapstructure:"async_resourcegroup_delete"`
@ -258,10 +258,10 @@ func (c *Config) createCertificate() (string, error) {
func newConfig(raws ...interface{}) (*Config, []string, error) {
var c Config
c.ctx.Funcs = TemplateFuncs
err := config.Decode(&c, &config.DecodeOpts{
Interpolate: true,
InterpolateContext: c.ctx,
InterpolateContext: &c.ctx,
}, raws...)
if err != nil {
@ -299,7 +299,7 @@ func newConfig(raws ...interface{}) (*Config, []string, error) {
}
var errs *packer.MultiError
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(c.ctx)...)
errs = packer.MultiErrorAppend(errs, c.Comm.Prepare(&c.ctx)...)
assertRequiredParametersSet(&c, errs)
assertTagProperties(&c, errs)

View file

@ -0,0 +1,43 @@
package arm
import (
"bytes"
"text/template"
)
func isValidByteValue(b byte) bool {
if '0' <= b && b <= '9' {
return true
}
if 'a' <= b && b <= 'z' {
return true
}
if 'A' <= b && b <= 'Z' {
return true
}
return b == '.' || b == '_' || b == '-'
}
// Clean up image name by replacing invalid characters with "-"
// Names are not allowed to end in '.', '-', or '_' and are trimmed.
func templateCleanImageName(s string) string {
if ok, _ := assertManagedImageName(s, ""); ok {
return s
}
b := []byte(s)
newb := make([]byte, len(b))
for i := range newb {
if isValidByteValue(b[i]) {
newb[i] = b[i]
} else {
newb[i] = '-'
}
}
newb = bytes.TrimRight(newb, "-_.")
return string(newb)
}
var TemplateFuncs = template.FuncMap{
"clean_image_name": templateCleanImageName,
}

View file

@ -0,0 +1,49 @@
package arm
import "testing"
func TestTemplateCleanImageName(t *testing.T) {
vals := []struct {
origName string
expected string
}{
// test that valid name is unchanged
{
origName: "abcde-012345xyz",
expected: "abcde-012345xyz",
},
// test that colons are converted to hyphens
{
origName: "abcde-012345v1.0:0",
expected: "abcde-012345v1.0-0",
},
// Name starting with number is not valid, but not in scope of this
// function to correct
{
origName: "012345v1.0:0",
expected: "012345v1.0-0",
},
// Name over 80 chars is not valid, but not corrected by this function.
{
origName: "l012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
expected: "l012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789",
},
// Name cannot end in a -Name over 80 chars is not valid, but not corrected by this function.
{
origName: "abcde-:_",
expected: "abcde",
},
// Lost of special characters
{
origName: "My()./-_:&^ $%[]#'@name",
expected: "My--.--_-----------name",
},
}
for _, v := range vals {
name := templateCleanImageName(v.origName)
if name != v.expected {
t.Fatalf("template names do not match: expected %s got %s\n", v.expected, name)
}
}
}

View file

@ -2,13 +2,19 @@ package arm
import (
"fmt"
"strings"
"github.com/hashicorp/packer/builder/azure/common"
)
const (
TempNameAlphabet = "0123456789bcdfghjklmnpqrstvwxyz"
TempPasswordAlphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
TempNameAlphabet = "0123456789bcdfghjklmnpqrstvwxyz"
numbers = "0123456789"
lowerCase = "abcdefghijklmnopqrstuvwxyz"
upperCase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
TempPasswordAlphabet = numbers + lowerCase + upperCase
)
type TempName struct {
@ -39,8 +45,37 @@ func NewTempName() *TempName {
tempName.VirtualNetworkName = fmt.Sprintf("pkrvn%s", suffix)
tempName.ResourceGroupName = fmt.Sprintf("packer-Resource-Group-%s", suffix)
tempName.AdminPassword = common.RandomString(TempPasswordAlphabet, 32)
tempName.AdminPassword = generatePassword()
tempName.CertificatePassword = common.RandomString(TempPasswordAlphabet, 32)
return tempName
}
// generate a password that is acceptable to Azure
// Three of the four items must be met.
// 1. Contains an uppercase character
// 2. Contains a lowercase character
// 3. Contains a numeric digit
// 4. Contains a special character
func generatePassword() string {
var s string
for i := 0; i < 100; i++ {
s := common.RandomString(TempPasswordAlphabet, 32)
if !strings.ContainsAny(s, numbers) {
continue
}
if !strings.ContainsAny(s, lowerCase) {
continue
}
if !strings.ContainsAny(s, upperCase) {
continue
}
return s
}
// if an acceptable password cannot be generated in 100 tries, give up
return s
}

View file

@ -41,6 +41,20 @@ func TestTempNameShouldCreatePrefixedRandomNames(t *testing.T) {
}
}
func TestTempAdminPassword(t *testing.T) {
tempName := NewTempName()
if !strings.ContainsAny(tempName.AdminPassword, numbers) {
t.Errorf("Expected AdminPassword to contain at least one of '%s'!", numbers)
}
if !strings.ContainsAny(tempName.AdminPassword, lowerCase) {
t.Errorf("Expected AdminPassword to contain at least one of '%s'!", lowerCase)
}
if !strings.ContainsAny(tempName.AdminPassword, upperCase) {
t.Errorf("Expected AdminPassword to contain at least one of '%s'!", upperCase)
}
}
func TestTempNameShouldHaveSameSuffix(t *testing.T) {
tempName := NewTempName()
suffix := tempName.ComputeName[5:]

View file

@ -27,26 +27,27 @@ type Config struct {
HTTPGetOnly bool `mapstructure:"http_get_only"`
SSLNoVerify bool `mapstructure:"ssl_no_verify"`
CIDRList []string `mapstructure:"cidr_list"`
CreateSecurityGroup bool `mapstructure:"create_security_group"`
DiskOffering string `mapstructure:"disk_offering"`
DiskSize int64 `mapstructure:"disk_size"`
Expunge bool `mapstructure:"expunge"`
Hypervisor string `mapstructure:"hypervisor"`
InstanceName string `mapstructure:"instance_name"`
Keypair string `mapstructure:"keypair"`
Network string `mapstructure:"network"`
Project string `mapstructure:"project"`
PublicIPAddress string `mapstructure:"public_ip_address"`
SecurityGroups []string `mapstructure:"security_groups"`
ServiceOffering string `mapstructure:"service_offering"`
SourceISO string `mapstructure:"source_iso"`
SourceTemplate string `mapstructure:"source_template"`
TemporaryKeypairName string `mapstructure:"temporary_keypair_name"`
UseLocalIPAddress bool `mapstructure:"use_local_ip_address"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
Zone string `mapstructure:"zone"`
CIDRList []string `mapstructure:"cidr_list"`
CreateSecurityGroup bool `mapstructure:"create_security_group"`
DiskOffering string `mapstructure:"disk_offering"`
DiskSize int64 `mapstructure:"disk_size"`
Expunge bool `mapstructure:"expunge"`
Hypervisor string `mapstructure:"hypervisor"`
InstanceName string `mapstructure:"instance_name"`
Keypair string `mapstructure:"keypair"`
Network string `mapstructure:"network"`
Project string `mapstructure:"project"`
PublicIPAddress string `mapstructure:"public_ip_address"`
SecurityGroups []string `mapstructure:"security_groups"`
ServiceOffering string `mapstructure:"service_offering"`
PreventFirewallChanges bool `mapstructure:"prevent_firewall_changes"`
SourceISO string `mapstructure:"source_iso"`
SourceTemplate string `mapstructure:"source_template"`
TemporaryKeypairName string `mapstructure:"temporary_keypair_name"`
UseLocalIPAddress bool `mapstructure:"use_local_ip_address"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
Zone string `mapstructure:"zone"`
TemplateName string `mapstructure:"template_name"`
TemplateDisplayText string `mapstructure:"template_display_text"`

View file

@ -117,6 +117,11 @@ func (s *stepSetupNetworking) Run(_ context.Context, state multistep.StateBag) m
// Store the port forward ID.
state.Put("port_forward_id", forward.Id)
if config.PreventFirewallChanges {
ui.Message("Networking has been setup (without firewall changes)!")
return multistep.ActionContinue
}
if network.Vpcid != "" {
ui.Message("Creating network ACL rule...")

View file

@ -47,7 +47,9 @@ func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) mu
p.SetDisplayname("Created by Packer")
if keypair, ok := state.GetOk("keypair"); ok {
p.SetKeypair(keypair.(string))
kp := keypair.(string)
ui.Message(fmt.Sprintf("Using keypair: %s", kp))
p.SetKeypair(kp)
}
if securitygroups, ok := state.GetOk("security_groups"); ok {
@ -120,6 +122,7 @@ func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) mu
}
ui.Message("Instance has been created!")
ui.Message(fmt.Sprintf("Instance ID: %s", instance.Id))
// In debug-mode, we output the password
if s.Debug {

View file

@ -51,7 +51,7 @@ func (s *stepCreateTemplate) Run(_ context.Context, state multistep.StateBag) mu
}
ui.Message("Retrieving the ROOT volume ID...")
volumeID, err := getRootVolumeID(client, instanceID)
volumeID, err := getRootVolumeID(client, instanceID, config.Project)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
@ -89,13 +89,16 @@ func (s *stepCreateTemplate) Cleanup(state multistep.StateBag) {
// Nothing to cleanup for this step.
}
func getRootVolumeID(client *cloudstack.CloudStackClient, instanceID string) (string, error) {
func getRootVolumeID(client *cloudstack.CloudStackClient, instanceID, projectID string) (string, error) {
// Retrieve the virtual machine object.
p := client.Volume.NewListVolumesParams()
// Set the type and virtual machine ID
p.SetType("ROOT")
p.SetVirtualmachineid(instanceID)
if projectID != "" {
p.SetProjectid(projectID)
}
volumes, err := client.Volume.ListVolumes(p)
if err != nil {

View file

@ -60,6 +60,12 @@ func (s *stepKeypair) Run(_ context.Context, state multistep.StateBag) multistep
ui.Say(fmt.Sprintf("Creating temporary keypair: %s ...", s.TemporaryKeyPairName))
p := client.SSH.NewCreateSSHKeyPairParams(s.TemporaryKeyPairName)
cfg := state.Get("config").(*Config)
if cfg.Project != "" {
p.SetProjectid(cfg.Project)
}
keypair, err := client.SSH.CreateSSHKeyPair(p)
if err != nil {
err := fmt.Errorf("Error creating temporary keypair: %s", err)
@ -120,12 +126,16 @@ func (s *stepKeypair) Cleanup(state multistep.StateBag) {
ui := state.Get("ui").(packer.Ui)
client := state.Get("client").(*cloudstack.CloudStackClient)
cfg := state.Get("config").(*Config)
p := client.SSH.NewDeleteSSHKeyPairParams(s.TemporaryKeyPairName)
if cfg.Project != "" {
p.SetProjectid(cfg.Project)
}
ui.Say(fmt.Sprintf("Deleting temporary keypair: %s ...", s.TemporaryKeyPairName))
_, err := client.SSH.DeleteSSHKeyPair(client.SSH.NewDeleteSSHKeyPairParams(
s.TemporaryKeyPairName,
))
_, err := client.SSH.DeleteSSHKeyPair(p)
if err != nil {
ui.Error(err.Error())
ui.Error(fmt.Sprintf(

View file

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"regexp"
"time"
"github.com/hashicorp/packer/common"
@ -35,6 +36,7 @@ type Config struct {
DropletName string `mapstructure:"droplet_name"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
Tags []string `mapstructure:"tags"`
ctx interpolate.Context
}
@ -121,6 +123,17 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
}
}
if c.Tags == nil {
c.Tags = make([]string, 0)
}
tagRe := regexp.MustCompile("^[[:alnum:]:_-]{1,255}$")
for _, t := range c.Tags {
if !tagRe.MatchString(t) {
errs = packer.MultiErrorAppend(errs, errors.New(fmt.Sprintf("invalid tag: %s", t)))
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, nil, errs
}

View file

@ -49,6 +49,7 @@ func (s *stepCreateDroplet) Run(_ context.Context, state multistep.StateBag) mul
Monitoring: c.Monitoring,
IPv6: c.IPv6,
UserData: userData,
Tags: c.Tags,
})
if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err)

View file

@ -1,7 +1,11 @@
package lxc
import (
"bytes"
"fmt"
"log"
"os/exec"
"strings"
)
// CommandWrapper is a type that given a command, will possibly modify that
@ -13,3 +17,25 @@ type CommandWrapper func(string) (string, error)
func ShellCommand(command string) *exec.Cmd {
return exec.Command("/bin/sh", "-c", command)
}
func RunCommand(args ...string) error {
var stdout, stderr bytes.Buffer
log.Printf("Executing args: %#v", args)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("Command error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return err
}

View file

@ -59,7 +59,6 @@ func (c *LxcAttachCommunicator) Start(cmd *packer.RemoteCmd) error {
}
func (c *LxcAttachCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
dst = filepath.Join(c.RootFs, dst)
log.Printf("Uploading to rootfs: %s", dst)
tf, err := ioutil.TempFile("", "packer-lxc-attach")
if err != nil {
@ -68,7 +67,11 @@ func (c *LxcAttachCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo)
defer os.Remove(tf.Name())
io.Copy(tf, r)
cpCmd, err := c.CmdWrapper(fmt.Sprintf("sudo cp %s %s", tf.Name(), dst))
attachCommand := []string{"cat", "%s", " | ", "lxc-attach"}
attachCommand = append(attachCommand, c.AttachOptions...)
attachCommand = append(attachCommand, []string{"--name", "%s", "--", "/bin/sh -c \"/bin/cat > %s\""}...)
cpCmd, err := c.CmdWrapper(fmt.Sprintf(strings.Join(attachCommand, " "), tf.Name(), c.ContainerName, dst))
if err != nil {
return err
}
@ -78,14 +81,14 @@ func (c *LxcAttachCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo)
// 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))
mvCmd, err := c.CmdWrapper(fmt.Sprintf("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))
cpCmd, err = c.CmdWrapper(fmt.Sprintf(strings.Join(attachCommand, " "), adjustedTempName, c.ContainerName, dst))
if err != nil {
return err
}
@ -100,7 +103,7 @@ func (c *LxcAttachCommunicator) UploadDir(dst string, src string, exclude []stri
// TODO: remove any file copied if it appears in `exclude`
dest := filepath.Join(c.RootFs, dst)
log.Printf("Uploading directory '%s' to rootfs '%s'", src, dest)
cpCmd, err := c.CmdWrapper(fmt.Sprintf("sudo cp -R %s/. %s", src, dest))
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp -R %s/. %s", src, dest))
if err != nil {
return err
}
@ -131,7 +134,7 @@ func (c *LxcAttachCommunicator) DownloadDir(src string, dst string, exclude []st
func (c *LxcAttachCommunicator) Execute(commandString string) (*exec.Cmd, error) {
log.Printf("Executing with lxc-attach in container: %s %s %s", c.ContainerName, c.RootFs, commandString)
attachCommand := []string{"sudo", "lxc-attach"}
attachCommand := []string{"lxc-attach"}
attachCommand = append(attachCommand, c.AttachOptions...)
attachCommand = append(attachCommand, []string{"--name", "%s", "--", "/bin/sh -c \"%s\""}...)

View file

@ -1,15 +1,13 @@
package lxc
import (
"bytes"
"context"
"fmt"
"io"
"log"
"os"
"os/exec"
"os/user"
"path/filepath"
"strings"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
@ -23,7 +21,16 @@ func (s *stepExport) Run(_ context.Context, state multistep.StateBag) multistep.
name := config.ContainerName
containerDir := fmt.Sprintf("/var/lib/lxc/%s", name)
lxc_dir := "/var/lib/lxc"
user, err := user.Current()
if err != nil {
log.Print("Cannot find current user. Falling back to /var/lib/lxc...")
}
if user.Uid != "0" && user.HomeDir != "" {
lxc_dir = filepath.Join(user.HomeDir, ".local", "share", "lxc")
}
containerDir := filepath.Join(lxc_dir, name)
outputPath := filepath.Join(config.OutputDir, "rootfs.tar.gz")
configFilePath := filepath.Join(config.OutputDir, "lxc-config")
@ -47,7 +54,7 @@ func (s *stepExport) Run(_ context.Context, state multistep.StateBag) multistep.
_, err = io.Copy(configFile, originalConfigFile)
commands := make([][]string, 4)
commands := make([][]string, 3)
commands[0] = []string{
"lxc-stop", "--name", name,
}
@ -57,13 +64,10 @@ func (s *stepExport) Run(_ context.Context, state multistep.StateBag) multistep.
commands[2] = []string{
"chmod", "+x", configFilePath,
}
commands[3] = []string{
"sh", "-c", "chown $USER:`id -gn` " + filepath.Join(config.OutputDir, "*"),
}
ui.Say("Exporting container...")
for _, command := range commands {
err := s.SudoCommand(command...)
err := RunCommand(command...)
if err != nil {
err := fmt.Errorf("Error exporting container: %s", err)
state.Put("error", err)
@ -76,25 +80,3 @@ func (s *stepExport) Run(_ context.Context, state multistep.StateBag) multistep.
}
func (s *stepExport) Cleanup(state multistep.StateBag) {}
func (s *stepExport) SudoCommand(args ...string) error {
var stdout, stderr bytes.Buffer
log.Printf("Executing sudo command: %#v", args)
cmd := exec.Command("sudo", args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("Sudo command error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return err
}

View file

@ -1,13 +1,11 @@
package lxc
import (
"bytes"
"context"
"fmt"
"log"
"os/exec"
"os/user"
"path/filepath"
"strings"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
@ -23,6 +21,13 @@ func (s *stepLxcCreate) Run(_ context.Context, state multistep.StateBag) multist
// TODO: read from env
lxc_dir := "/var/lib/lxc"
user, err := user.Current()
if err != nil {
log.Print("Cannot find current user. Falling back to /var/lib/lxc...")
}
if user.Uid != "0" && user.HomeDir != "" {
lxc_dir = filepath.Join(user.HomeDir, ".local", "share", "lxc")
}
rootfs := filepath.Join(lxc_dir, name, "rootfs")
if config.PackerForce {
@ -30,7 +35,9 @@ func (s *stepLxcCreate) Run(_ context.Context, state multistep.StateBag) multist
}
commands := make([][]string, 3)
commands[0] = append(config.EnvVars, "lxc-create")
commands[0] = append(commands[0], "env")
commands[0] = append(commands[0], config.EnvVars...)
commands[0] = append(commands[0], "lxc-create")
commands[0] = append(commands[0], config.CreateOptions...)
commands[0] = append(commands[0], []string{"-n", name, "-t", config.Name, "--"}...)
commands[0] = append(commands[0], config.Parameters...)
@ -42,8 +49,7 @@ func (s *stepLxcCreate) Run(_ context.Context, state multistep.StateBag) multist
ui.Say("Creating container...")
for _, command := range commands {
log.Printf("Executing sudo command: %#v", command)
err := s.SudoCommand(command...)
err := RunCommand(command...)
if err != nil {
err := fmt.Errorf("Error creating container: %s", err)
state.Put("error", err)
@ -66,29 +72,7 @@ func (s *stepLxcCreate) Cleanup(state multistep.StateBag) {
}
ui.Say("Unregistering and deleting virtual machine...")
if err := s.SudoCommand(command...); err != nil {
if err := RunCommand(command...); err != nil {
ui.Error(fmt.Sprintf("Error deleting virtual machine: %s", err))
}
}
func (s *stepLxcCreate) SudoCommand(args ...string) error {
var stdout, stderr bytes.Buffer
log.Printf("Executing sudo command: %#v", args)
cmd := exec.Command("sudo", args...)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
stdoutString := strings.TrimSpace(stdout.String())
stderrString := strings.TrimSpace(stderr.String())
if _, ok := err.(*exec.ExitError); ok {
err = fmt.Errorf("Sudo command error: %s", stderrString)
}
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)
return err
}

View file

@ -26,7 +26,7 @@ func (s *stepLxdLaunch) Run(_ context.Context, state multistep.StateBag) multist
}
for k, v := range config.LaunchConfig {
launch_args = append(launch_args, fmt.Sprintf("--config %s=%s", k, v))
launch_args = append(launch_args, "--config", fmt.Sprintf("%s=%s", k, v))
}
ui.Say("Creating container...")

View file

@ -194,6 +194,20 @@ func (c *AccessConfig) imageV2Client() (*gophercloud.ServiceClient, error) {
})
}
func (c *AccessConfig) blockStorageV3Client() (*gophercloud.ServiceClient, error) {
return openstack.NewBlockStorageV3(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
}
func (c *AccessConfig) networkV2Client() (*gophercloud.ServiceClient, error) {
return openstack.NewNetworkV2(c.osClient, gophercloud.EndpointOpts{
Region: c.Region,
Availability: c.getEndpointType(),
})
}
func (c *AccessConfig) getEndpointType() gophercloud.Availability {
if c.EndpointType == "internal" || c.EndpointType == "internalURL" {
return gophercloud.AvailabilityInternal

42
builder/openstack/builder.go Executable file → Normal file
View file

@ -86,17 +86,26 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
PrivateKeyFile: b.config.RunConfig.Comm.SSHPrivateKey,
SSHAgentAuth: b.config.RunConfig.Comm.SSHAgentAuth,
},
&StepCreateVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
SourceImage: b.config.SourceImage,
VolumeName: b.config.VolumeName,
VolumeType: b.config.VolumeType,
VolumeAvailabilityZone: b.config.VolumeAvailabilityZone,
},
&StepRunSourceServer{
Name: b.config.InstanceName,
SourceImage: b.config.SourceImage,
SourceImageName: b.config.SourceImageName,
SecurityGroups: b.config.SecurityGroups,
Networks: b.config.Networks,
AvailabilityZone: b.config.AvailabilityZone,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
ConfigDrive: b.config.ConfigDrive,
InstanceMetadata: b.config.InstanceMetadata,
Name: b.config.InstanceName,
SourceImage: b.config.SourceImage,
SourceImageName: b.config.SourceImageName,
SecurityGroups: b.config.SecurityGroups,
Networks: b.config.Networks,
Ports: b.config.Ports,
AvailabilityZone: b.config.AvailabilityZone,
UserData: b.config.UserData,
UserDataFile: b.config.UserDataFile,
ConfigDrive: b.config.ConfigDrive,
InstanceMetadata: b.config.InstanceMetadata,
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&StepGetPassword{
Debug: b.config.PackerDebug,
@ -106,9 +115,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Wait: b.config.RackconnectWait,
},
&StepAllocateIp{
FloatingIpPool: b.config.FloatingIpPool,
FloatingIp: b.config.FloatingIp,
ReuseIps: b.config.ReuseIps,
FloatingIPNetwork: b.config.FloatingIPNetwork,
FloatingIP: b.config.FloatingIP,
ReuseIPs: b.config.ReuseIPs,
},
&communicator.StepConnect{
Config: &b.config.RunConfig.Comm,
@ -123,7 +132,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
},
&common.StepProvision{},
&StepStopServer{},
&stepCreateImage{},
&StepDetachVolume{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&stepCreateImage{
UseBlockStorageVolume: b.config.UseBlockStorageVolume,
},
&stepUpdateImageVisibility{},
&stepAddImageMembers{},
}

View file

@ -0,0 +1,125 @@
package openstack
import (
"fmt"
"github.com/google/uuid"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/attachinterfaces"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/external"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/gophercloud/gophercloud/openstack/networking/v2/networks"
"github.com/gophercloud/gophercloud/pagination"
)
// CheckFloatingIP gets a floating IP by its ID and checks if it is already
// associated with any internal interface.
// It returns floating IP if it can be used.
func CheckFloatingIP(client *gophercloud.ServiceClient, id string) (*floatingips.FloatingIP, error) {
floatingIP, err := floatingips.Get(client, id).Extract()
if err != nil {
return nil, err
}
if floatingIP.PortID != "" {
return nil, fmt.Errorf("provided floating IP '%s' is already associated with port '%s'",
id, floatingIP.PortID)
}
return floatingIP, nil
}
// FindFreeFloatingIP returns free unassociated floating IP.
// It will return first floating IP if there are many.
func FindFreeFloatingIP(client *gophercloud.ServiceClient) (*floatingips.FloatingIP, error) {
var freeFloatingIP *floatingips.FloatingIP
pager := floatingips.List(client, floatingips.ListOpts{
Status: "DOWN",
})
err := pager.EachPage(func(page pagination.Page) (bool, error) {
candidates, err := floatingips.ExtractFloatingIPs(page)
if err != nil {
return false, err // stop and throw error out
}
for _, candidate := range candidates {
if candidate.PortID != "" {
continue // this floating IP is associated with port, move to next in list
}
// Floating IP is able to be allocated.
freeFloatingIP = &candidate
return false, nil // stop iterating over pages
}
return true, nil // try the next page
})
if err != nil {
return nil, err
}
if freeFloatingIP == nil {
return nil, fmt.Errorf("no free floating IPs found")
}
return freeFloatingIP, nil
}
// GetInstancePortID returns internal port of the instance that can be used for
// the association of a floating IP.
// It will return an ID of a first port if there are many.
func GetInstancePortID(client *gophercloud.ServiceClient, id string) (string, error) {
interfacesPage, err := attachinterfaces.List(client, id).AllPages()
if err != nil {
return "", err
}
interfaces, err := attachinterfaces.ExtractInterfaces(interfacesPage)
if err != nil {
return "", err
}
if len(interfaces) == 0 {
return "", fmt.Errorf("instance '%s' has no interfaces", id)
}
return interfaces[0].PortID, nil
}
// CheckFloatingIPNetwork checks provided network reference and returns a valid
// Networking service ID.
func CheckFloatingIPNetwork(client *gophercloud.ServiceClient, networkRef string) (string, error) {
if _, err := uuid.Parse(networkRef); err != nil {
return GetFloatingIPNetworkIDByName(client, networkRef)
}
return networkRef, nil
}
// ExternalNetwork is a network with external router.
type ExternalNetwork struct {
networks.Network
external.NetworkExternalExt
}
// GetFloatingIPNetworkIDByName searches for the external network ID by the provided name.
func GetFloatingIPNetworkIDByName(client *gophercloud.ServiceClient, networkName string) (string, error) {
var externalNetworks []ExternalNetwork
allPages, err := networks.List(client, networks.ListOpts{
Name: networkName,
}).AllPages()
if err != nil {
return "", err
}
if err := networks.ExtractNetworksInto(allPages, &externalNetworks); err != nil {
return "", err
}
if len(externalNetworks) == 0 {
return "", fmt.Errorf("can't find external network %s", networkName)
}
// Check and return the first external network.
if !externalNetworks[0].External {
return "", fmt.Errorf("network %s is not external", networkName)
}
return externalNetworks[0].ID, nil
}

View file

@ -18,23 +18,32 @@ type RunConfig struct {
SSHInterface string `mapstructure:"ssh_interface"`
SSHIPVersion string `mapstructure:"ssh_ip_version"`
SourceImage string `mapstructure:"source_image"`
SourceImageName string `mapstructure:"source_image_name"`
Flavor string `mapstructure:"flavor"`
AvailabilityZone string `mapstructure:"availability_zone"`
RackconnectWait bool `mapstructure:"rackconnect_wait"`
FloatingIpPool string `mapstructure:"floating_ip_pool"`
FloatingIp string `mapstructure:"floating_ip"`
ReuseIps bool `mapstructure:"reuse_ips"`
SecurityGroups []string `mapstructure:"security_groups"`
Networks []string `mapstructure:"networks"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
InstanceName string `mapstructure:"instance_name"`
InstanceMetadata map[string]string `mapstructure:"instance_metadata"`
SourceImage string `mapstructure:"source_image"`
SourceImageName string `mapstructure:"source_image_name"`
Flavor string `mapstructure:"flavor"`
AvailabilityZone string `mapstructure:"availability_zone"`
RackconnectWait bool `mapstructure:"rackconnect_wait"`
FloatingIPNetwork string `mapstructure:"floating_ip_network"`
FloatingIP string `mapstructure:"floating_ip"`
ReuseIPs bool `mapstructure:"reuse_ips"`
SecurityGroups []string `mapstructure:"security_groups"`
Networks []string `mapstructure:"networks"`
Ports []string `mapstructure:"ports"`
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`
InstanceName string `mapstructure:"instance_name"`
InstanceMetadata map[string]string `mapstructure:"instance_metadata"`
ConfigDrive bool `mapstructure:"config_drive"`
// Used for BC, value will be passed to the "floating_ip_network"
FloatingIPPool string `mapstructure:"floating_ip_pool"`
UseBlockStorageVolume bool `mapstructure:"use_blockstorage_volume"`
VolumeName string `mapstructure:"volume_name"`
VolumeType string `mapstructure:"volume_type"`
VolumeAvailabilityZone string `mapstructure:"volume_availability_zone"`
// Not really used, but here for BC
OpenstackProvider string `mapstructure:"openstack_provider"`
UseFloatingIp bool `mapstructure:"use_floating_ip"`
@ -51,8 +60,8 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
c.TemporaryKeyPairName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
if c.UseFloatingIp && c.FloatingIpPool == "" {
c.FloatingIpPool = "public"
if c.FloatingIPPool != "" && c.FloatingIPNetwork == "" {
c.FloatingIPNetwork = c.FloatingIPPool
}
// Validation
@ -89,5 +98,18 @@ func (c *RunConfig) Prepare(ctx *interpolate.Context) []error {
}
}
if c.UseBlockStorageVolume {
// Use Compute instance availability zone for the Block Storage volume if
// it's not provided.
if c.VolumeAvailabilityZone == "" {
c.VolumeAvailabilityZone = c.AvailabilityZone
}
// Use random name for the Block Storage volume if it's not provided.
if c.VolumeName == "" {
c.VolumeName = fmt.Sprintf("packer_%s", uuid.TimeOrderedUUID())
}
}
return errs
}

View file

@ -70,3 +70,60 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
t.Fatalf("invalid value: %d", c.Comm.SSHPort)
}
}
func TestRunConfigPrepare_BlockStorage(t *testing.T) {
c := testRunConfig()
c.UseBlockStorageVolume = true
c.VolumeType = "fast"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeType != "fast" {
t.Fatalf("invalid value: %s", c.VolumeType)
}
c.AvailabilityZone = "RegionTwo"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeAvailabilityZone != "RegionTwo" {
t.Fatalf("invalid value: %s", c.VolumeAvailabilityZone)
}
c.VolumeAvailabilityZone = "RegionOne"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.VolumeAvailabilityZone != "RegionOne" {
t.Fatalf("invalid value: %s", c.VolumeAvailabilityZone)
}
c.VolumeName = "PackerVolume"
if c.VolumeName != "PackerVolume" {
t.Fatalf("invalid value: %s", c.VolumeName)
}
}
func TestRunConfigPrepare_FloatingIPPoolCompat(t *testing.T) {
c := testRunConfig()
c.FloatingIPPool = "uuid1"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.FloatingIPNetwork != "uuid1" {
t.Fatalf("invalid value: %s", c.FloatingIPNetwork)
}
c.FloatingIPNetwork = "uuid2"
c.FloatingIPPool = "uuid3"
if err := c.Prepare(nil); len(err) != 0 {
t.Fatalf("err: %s", err)
}
if c.FloatingIPNetwork != "uuid2" {
t.Fatalf("invalid value: %s", c.FloatingIPNetwork)
}
}

View file

@ -9,8 +9,8 @@ import (
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
packerssh "github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/multistep"
"golang.org/x/crypto/ssh"
@ -35,9 +35,9 @@ func CommHost(
// If we have a floating IP, use that
ip := state.Get("access_ip").(*floatingips.FloatingIP)
if ip != nil && ip.IP != "" {
log.Printf("[DEBUG] Using floating IP %s to connect", ip.IP)
return ip.IP, nil
if ip != nil && ip.FloatingIP != "" {
log.Printf("[DEBUG] Using floating IP %s to connect", ip.FloatingIP)
return ip.FloatingIP, nil
}
if s.AccessIPv4 != "" {

View file

@ -4,17 +4,16 @@ import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/floatingips"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/gophercloud/gophercloud/pagination"
"github.com/gophercloud/gophercloud/openstack/networking/v2/extensions/layer3/floatingips"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepAllocateIp struct {
FloatingIpPool string
FloatingIp string
ReuseIps bool
FloatingIPNetwork string
FloatingIP string
ReuseIPs bool
}
func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -23,123 +22,146 @@ func (s *StepAllocateIp) Run(_ context.Context, state multistep.StateBag) multis
server := state.Get("server").(*servers.Server)
// We need the v2 compute client
client, err := config.computeV2Client()
computeClient, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
var instanceIp floatingips.FloatingIP
// We need the v2 network client
networkClient, err := config.networkV2Client()
if err != nil {
err = fmt.Errorf("Error initializing network client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
var instanceIP floatingips.FloatingIP
// This is here in case we error out before putting instanceIp into the
// statebag below, because it is requested by Cleanup()
state.Put("access_ip", &instanceIp)
state.Put("access_ip", &instanceIP)
if s.FloatingIp != "" {
instanceIp.IP = s.FloatingIp
} else if s.FloatingIpPool != "" {
// If ReuseIps is set to true and we have a free floating IP in
// the pool, use it first rather than creating one
if s.ReuseIps {
ui.Say(fmt.Sprintf("Searching for unassociated floating IP in pool %s", s.FloatingIpPool))
pager := floatingips.List(client)
err := pager.EachPage(func(page pagination.Page) (bool, error) {
candidates, err := floatingips.ExtractFloatingIPs(page)
if err != nil {
return false, err // stop and throw error out
}
for _, candidate := range candidates {
if candidate.Pool != s.FloatingIpPool || candidate.InstanceID != "" {
continue // move to next in list
}
// In correct pool and able to be allocated
instanceIp.IP = candidate.IP
ui.Message(fmt.Sprintf("Selected floating IP: %s", instanceIp.IP))
state.Put("floatingip_istemp", false)
return false, nil // stop iterating over pages
}
return true, nil // try the next page
})
if err != nil {
err := fmt.Errorf("Error searching for floating ip from pool '%s'", s.FloatingIpPool)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Try to Use the OpenStack floating IP by checking provided parameters in
// the following order:
// - try to use "FloatingIP" ID directly if it's provided
// - try to find free floating IP in the project if "ReuseIPs" is set
// - create a new floating IP if "FloatingIPNetwork" is provided (it can be
// ID or name of the network).
if s.FloatingIP != "" {
// Try to use FloatingIP if it was provided by the user.
freeFloatingIP, err := CheckFloatingIP(networkClient, s.FloatingIP)
if err != nil {
err := fmt.Errorf("Error using provided floating IP '%s': %s", s.FloatingIP, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if instanceIp.IP == "" {
ui.Say(fmt.Sprintf("Creating floating IP..."))
ui.Message(fmt.Sprintf("Pool: %s", s.FloatingIpPool))
newIp, err := floatingips.Create(client, floatingips.CreateOpts{
Pool: s.FloatingIpPool,
}).Extract()
if err != nil {
err := fmt.Errorf("Error creating floating ip from pool '%s'", s.FloatingIpPool)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceIp = *newIp
ui.Message(fmt.Sprintf("Created floating IP: %s", instanceIp.IP))
state.Put("floatingip_istemp", true)
instanceIP = *freeFloatingIP
ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
state.Put("floatingip_istemp", false)
} else if s.ReuseIPs {
// If ReuseIPs is set to true and we have a free floating IP, use it rather
// than creating one.
ui.Say(fmt.Sprint("Searching for unassociated floating IP"))
freeFloatingIP, err := FindFreeFloatingIP(networkClient)
if err != nil {
err := fmt.Errorf("Error searching for floating IP: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceIP = *freeFloatingIP
ui.Message(fmt.Sprintf("Selected floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
state.Put("floatingip_istemp", false)
} else if s.FloatingIPNetwork != "" {
// Lastly, if FloatingIPNetwork was provided by the user, we need to use it
// to allocate a new floating IP and associate it to the instance.
floatingNetwork, err := CheckFloatingIPNetwork(networkClient, s.FloatingIPNetwork)
if err != nil {
err := fmt.Errorf("Error using the provided floating_ip_network: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("Creating floating IP using network %s ...", floatingNetwork))
newIP, err := floatingips.Create(networkClient, floatingips.CreateOpts{
FloatingNetworkID: floatingNetwork,
}).Extract()
if err != nil {
err := fmt.Errorf("Error creating floating IP from floating network '%s': %s", floatingNetwork, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
instanceIP = *newIP
ui.Message(fmt.Sprintf("Created floating IP: '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
state.Put("floatingip_istemp", true)
}
if instanceIp.IP != "" {
ui.Say(fmt.Sprintf("Associating floating IP with server..."))
ui.Message(fmt.Sprintf("IP: %s", instanceIp.IP))
err := floatingips.AssociateInstance(client, server.ID, floatingips.AssociateOpts{
FloatingIP: instanceIp.IP,
}).ExtractErr()
// Assoctate a floating IP if it was obtained in the previous steps.
if instanceIP.ID != "" {
ui.Say(fmt.Sprintf("Associating floating IP '%s' (%s) with instance port...",
instanceIP.ID, instanceIP.FloatingIP))
portID, err := GetInstancePortID(computeClient, server.ID)
if err != nil {
err := fmt.Errorf("Error getting interfaces of the instance '%s': %s", server.ID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
_, err = floatingips.Update(networkClient, instanceIP.ID, floatingips.UpdateOpts{
PortID: &portID,
}).Extract()
if err != nil {
err := fmt.Errorf(
"Error associating floating IP %s with instance: %s",
instanceIp.IP, err)
"Error associating floating IP '%s' (%s) with instance port '%s': %s",
instanceIP.ID, instanceIP.FloatingIP, portID, err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Message(fmt.Sprintf(
"Added floating IP %s to instance!", instanceIp.IP))
"Added floating IP '%s' (%s) to instance!", instanceIP.ID, instanceIP.FloatingIP))
}
state.Put("access_ip", &instanceIp)
state.Put("access_ip", &instanceIP)
return multistep.ActionContinue
}
func (s *StepAllocateIp) Cleanup(state multistep.StateBag) {
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
instanceIp := state.Get("access_ip").(*floatingips.FloatingIP)
instanceIP := state.Get("access_ip").(*floatingips.FloatingIP)
// Don't delete pool addresses we didn't allocate
if state.Get("floatingip_istemp") == false {
return
}
// We need the v2 compute client
client, err := config.computeV2Client()
// We need the v2 network client
client, err := config.networkV2Client()
if err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
"Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
return
}
if s.FloatingIpPool != "" && instanceIp.ID != "" {
if err := floatingips.Delete(client, instanceIp.ID).ExtractErr(); err != nil {
if instanceIP.ID != "" {
if err := floatingips.Delete(client, instanceIP.ID).ExtractErr(); err != nil {
ui.Error(fmt.Sprintf(
"Error deleting temporary floating IP %s", instanceIp.IP))
"Error deleting temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
return
}
ui.Say(fmt.Sprintf("Deleted temporary floating IP %s", instanceIp.IP))
ui.Say(fmt.Sprintf("Deleted temporary floating IP '%s' (%s)", instanceIP.ID, instanceIP.FloatingIP))
}
}

View file

@ -6,6 +6,8 @@ import (
"log"
"time"
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
@ -13,7 +15,9 @@ import (
"github.com/hashicorp/packer/packer"
)
type stepCreateImage struct{}
type stepCreateImage struct {
UseBlockStorageVolume bool
}
func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
config := state.Get("config").(Config)
@ -28,17 +32,41 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
return multistep.ActionHalt
}
// Create the image
// Create the image.
// Image source depends on the type of the Compute instance. It can be
// Block Storage service volume or regular Compute service local volume.
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
imageId, err := servers.CreateImage(client, server.ID, servers.CreateImageOpts{
Name: config.ImageName,
Metadata: config.ImageMetadata,
}).ExtractImageID()
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
var imageId string
if s.UseBlockStorageVolume {
// We need the v3 block storage client.
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
err = fmt.Errorf("Error initializing block storage client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
volume := state.Get("volume_id").(string)
image, err := volumeactions.UploadImage(blockStorageClient, volume, volumeactions.UploadImageOpts{
ImageName: config.ImageName,
}).Extract()
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
imageId = image.ImageID
} else {
imageId, err = servers.CreateImage(client, server.ID, servers.CreateImageOpts{
Name: config.ImageName,
Metadata: config.ImageMetadata,
}).ExtractImageID()
if err != nil {
err := fmt.Errorf("Error creating image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
// Set the Image ID in the state

View file

@ -0,0 +1,111 @@
package openstack
import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepCreateVolume struct {
UseBlockStorageVolume bool
SourceImage string
VolumeName string
VolumeType string
VolumeAvailabilityZone string
volumeID string
doCleanup bool
}
func (s *StepCreateVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// Proceed only if block storage volume is required.
if !s.UseBlockStorageVolume {
return multistep.ActionContinue
}
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
// We will need Block Storage and Image services clients.
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
err = fmt.Errorf("Error initializing block storage client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
imageClient, err := config.imageV2Client()
if err != nil {
err = fmt.Errorf("Error initializing image client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
// Get needed volume size from the source image.
volumeSize, err := GetVolumeSize(imageClient, s.SourceImage)
if err != nil {
err := fmt.Errorf("Error creating volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say("Creating volume...")
volumeOpts := volumes.CreateOpts{
Size: volumeSize,
VolumeType: s.VolumeType,
AvailabilityZone: s.VolumeAvailabilityZone,
Name: s.VolumeName,
ImageID: s.SourceImage,
}
volume, err := volumes.Create(blockStorageClient, volumeOpts).Extract()
if err != nil {
err := fmt.Errorf("Error creating volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for volume to become available.
ui.Say(fmt.Sprintf("Waiting for volume %s (volume id: %s) to become available...", config.VolumeName, volume.ID))
if err := WaitForVolume(blockStorageClient, volume.ID); err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Volume was created, so remember to clean it up.
s.doCleanup = true
// Set the Volume ID in the state.
ui.Message(fmt.Sprintf("Volume ID: %s", volume.ID))
state.Put("volume_id", volume.ID)
s.volumeID = volume.ID
return multistep.ActionContinue
}
func (s *StepCreateVolume) Cleanup(state multistep.StateBag) {
if !s.doCleanup {
return
}
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up volume. Please delete the volume manually: %s", s.volumeID))
return
}
ui.Say(fmt.Sprintf("Deleting volume: %s ...", s.volumeID))
err = volumes.Delete(blockStorageClient, s.volumeID).ExtractErr()
if err != nil {
ui.Error(fmt.Sprintf(
"Error cleaning up volume. Please delete the volume manually: %s", s.volumeID))
}
}

View file

@ -0,0 +1,54 @@
package openstack
import (
"context"
"fmt"
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
type StepDetachVolume struct {
UseBlockStorageVolume bool
}
func (s *StepDetachVolume) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
// Proceed only if block storage volume is used.
if !s.UseBlockStorageVolume {
return multistep.ActionContinue
}
config := state.Get("config").(Config)
ui := state.Get("ui").(packer.Ui)
blockStorageClient, err := config.blockStorageV3Client()
if err != nil {
err = fmt.Errorf("Error initializing block storage client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
volume := state.Get("volume_id").(string)
ui.Say(fmt.Sprintf("Detaching volume %s (volume id: %s)", config.VolumeName, volume))
if err := volumeactions.Detach(blockStorageClient, volume, volumeactions.DetachOpts{}).ExtractErr(); err != nil {
err = fmt.Errorf("Error detaching block storage volume: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
// Wait for volume to become available.
ui.Say(fmt.Sprintf("Waiting for volume %s (volume id: %s) to become available...", config.VolumeName, volume))
if err := WaitForVolume(blockStorageClient, volume); err != nil {
err := fmt.Errorf("Error waiting for volume: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *StepDetachVolume) Cleanup(multistep.StateBag) {
// No cleanup.
}

View file

@ -6,6 +6,7 @@ import (
"io/ioutil"
"log"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/bootfromvolume"
"github.com/gophercloud/gophercloud/openstack/compute/v2/extensions/keypairs"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
"github.com/hashicorp/packer/helper/multistep"
@ -13,17 +14,19 @@ import (
)
type StepRunSourceServer struct {
Name string
SourceImage string
SourceImageName string
SecurityGroups []string
Networks []string
AvailabilityZone string
UserData string
UserDataFile string
ConfigDrive bool
InstanceMetadata map[string]string
server *servers.Server
Name string
SourceImage string
SourceImageName string
SecurityGroups []string
Networks []string
Ports []string
AvailabilityZone string
UserData string
UserDataFile string
ConfigDrive bool
InstanceMetadata map[string]string
UseBlockStorageVolume bool
server *servers.Server
}
func (s *StepRunSourceServer) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@ -39,9 +42,13 @@ func (s *StepRunSourceServer) Run(_ context.Context, state multistep.StateBag) m
return multistep.ActionHalt
}
networks := make([]servers.Network, len(s.Networks))
for i, networkUuid := range s.Networks {
networks[i].UUID = networkUuid
networks := make([]servers.Network, len(s.Networks)+len(s.Ports))
i := 0
for ; i < len(s.Ports); i++ {
networks[i].Port = s.Ports[i]
}
for ; i < len(networks); i++ {
networks[i].UUID = s.Networks[i]
}
userData := []byte(s.UserData)
@ -69,18 +76,40 @@ func (s *StepRunSourceServer) Run(_ context.Context, state multistep.StateBag) m
ServiceClient: computeClient,
Metadata: s.InstanceMetadata,
}
var serverOptsExt servers.CreateOptsBuilder
keyName, hasKey := state.GetOk("keyPair")
if hasKey {
serverOptsExt = keypairs.CreateOptsExt{
// Create root volume in the Block Storage service if required.
// Add block device mapping v2 to the server create options if required.
if s.UseBlockStorageVolume {
volume := state.Get("volume_id").(string)
blockDeviceMappingV2 := []bootfromvolume.BlockDevice{
{
BootIndex: 0,
DestinationType: bootfromvolume.DestinationVolume,
SourceType: bootfromvolume.SourceVolume,
UUID: volume,
},
}
// ImageRef and block device mapping is an invalid options combination.
serverOpts.ImageRef = ""
serverOptsExt = bootfromvolume.CreateOptsExt{
CreateOptsBuilder: serverOpts,
KeyName: keyName.(string),
BlockDevice: blockDeviceMappingV2,
}
} else {
serverOptsExt = serverOpts
}
// Add keypair to the server create options.
keyName, hasKey := state.GetOk("keyPair")
if hasKey {
serverOptsExt = keypairs.CreateOptsExt{
CreateOptsBuilder: serverOptsExt,
KeyName: keyName.(string),
}
}
ui.Say("Launching server...")
s.server, err = servers.Create(computeClient, serverOptsExt).Extract()
if err != nil {
err := fmt.Errorf("Error launching source server: %s", err)

0
builder/openstack/step_stop_server.go Executable file → Normal file
View file

View file

@ -0,0 +1,67 @@
package openstack
import (
"log"
"time"
"github.com/gophercloud/gophercloud"
"github.com/gophercloud/gophercloud/openstack/blockstorage/v3/volumes"
"github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
)
// WaitForVolume waits for the given volume to become available.
func WaitForVolume(blockStorageClient *gophercloud.ServiceClient, volumeID string) error {
maxNumErrors := 10
numErrors := 0
for {
volume, err := volumes.Get(blockStorageClient, volumeID).Extract()
if err != nil {
errCode, ok := err.(*gophercloud.ErrUnexpectedResponseCode)
if ok && (errCode.Actual == 500 || errCode.Actual == 404) {
numErrors++
if numErrors >= maxNumErrors {
log.Printf("[ERROR] Maximum number of errors (%d) reached; failing with: %s", numErrors, err)
return err
}
log.Printf("[ERROR] %d error received, will ignore and retry: %s", errCode.Actual, err)
time.Sleep(2 * time.Second)
continue
}
return err
}
if volume.Status == "available" {
return nil
}
log.Printf("Waiting for volume creation status: %s", volume.Status)
time.Sleep(2 * time.Second)
}
}
// GetVolumeSize returns volume size in gigabytes based on the image min disk
// value if it's not empty.
// Or it calculates needed gigabytes size from the image bytes size.
func GetVolumeSize(imageClient *gophercloud.ServiceClient, imageID string) (int, error) {
sourceImage, err := images.Get(imageClient, imageID).Extract()
if err != nil {
return 0, err
}
if sourceImage.MinDiskGigabytes != 0 {
return sourceImage.MinDiskGigabytes, nil
}
volumeSizeMB := sourceImage.SizeBytes / 1024 / 1024
volumeSizeGB := int(sourceImage.SizeBytes / 1024 / 1024 / 1024)
// Increment gigabytes size if the initial size can't be divided without
// remainder.
if volumeSizeMB%1024 > 0 {
volumeSizeGB++
}
return volumeSizeGB, nil
}

View file

@ -1,6 +1,7 @@
package oci
import (
"context"
"fmt"
"github.com/oracle/oci-go-sdk/core"
@ -41,11 +42,12 @@ func (a *Artifact) String() string {
)
}
// State ...
func (a *Artifact) State(name string) interface{} {
return nil
}
// Destroy deletes the custom image associated with the artifact.
func (a *Artifact) Destroy() error {
return a.driver.DeleteImage(*a.Image.Id)
return a.driver.DeleteImage(context.TODO(), *a.Image.Id)
}

View file

@ -44,6 +44,17 @@ type Config struct {
BaseImageID string `mapstructure:"base_image_ocid"`
Shape string `mapstructure:"shape"`
ImageName string `mapstructure:"image_name"`
// Instance
InstanceName string `mapstructure:"instance_name"`
// Metadata optionally contains custom metadata key/value pairs provided in the
// configuration. While this can be used to set metadata["user_data"] the explicit
// "user_data" and "user_data_file" values will have precedence.
// An instance's metadata can be obtained from at http://169.254.169.254 on the
// launched instance.
Metadata map[string]string `mapstructure:"metadata"`
// UserData and UserDataFile file are both optional and mutually exclusive.
UserData string `mapstructure:"user_data"`
UserDataFile string `mapstructure:"user_data_file"`

View file

@ -29,11 +29,14 @@ func testConfig(accessConfFile *os.File) map[string]interface{} {
// Comm
"ssh_username": "opc",
"use_private_ip": false,
"metadata": map[string]string{
"key": "value",
},
}
}
func TestConfig(t *testing.T) {
// Shared set-up and defered deletion
// Shared set-up and deferred deletion
cfg, keyFile, err := baseTestConfigWithTmpKeyFile()
if err != nil {

View file

@ -1,14 +1,18 @@
package oci
import "github.com/oracle/oci-go-sdk/core"
import (
"context"
"github.com/oracle/oci-go-sdk/core"
)
// Driver interfaces between the builder steps and the OCI SDK.
type Driver interface {
CreateInstance(publicKey string) (string, error)
CreateImage(id string) (core.Image, error)
DeleteImage(id string) error
GetInstanceIP(id string) (string, error)
TerminateInstance(id string) error
WaitForImageCreation(id string) error
WaitForInstanceState(id string, waitStates []string, terminalState string) error
CreateInstance(ctx context.Context, publicKey string) (string, error)
CreateImage(ctx context.Context, id string) (core.Image, error)
DeleteImage(ctx context.Context, id string) error
GetInstanceIP(ctx context.Context, id string) (string, error)
TerminateInstance(ctx context.Context, id string) error
WaitForImageCreation(ctx context.Context, id string) error
WaitForInstanceState(ctx context.Context, id string, waitStates []string, terminalState string) error
}

View file

@ -1,6 +1,10 @@
package oci
import "github.com/oracle/oci-go-sdk/core"
import (
"context"
"github.com/oracle/oci-go-sdk/core"
)
// driverMock implements the Driver interface and communicates with Oracle
// OCI.
@ -27,7 +31,7 @@ type driverMock struct {
}
// CreateInstance creates a new compute instance.
func (d *driverMock) CreateInstance(publicKey string) (string, error) {
func (d *driverMock) CreateInstance(ctx context.Context, publicKey string) (string, error) {
if d.CreateInstanceErr != nil {
return "", d.CreateInstanceErr
}
@ -38,7 +42,7 @@ func (d *driverMock) CreateInstance(publicKey string) (string, error) {
}
// CreateImage creates a new custom image.
func (d *driverMock) CreateImage(id string) (core.Image, error) {
func (d *driverMock) CreateImage(ctx context.Context, id string) (core.Image, error) {
if d.CreateImageErr != nil {
return core.Image{}, d.CreateImageErr
}
@ -47,7 +51,7 @@ func (d *driverMock) CreateImage(id string) (core.Image, error) {
}
// DeleteImage mocks deleting a custom image.
func (d *driverMock) DeleteImage(id string) error {
func (d *driverMock) DeleteImage(ctx context.Context, id string) error {
if d.DeleteImageErr != nil {
return d.DeleteImageErr
}
@ -58,7 +62,7 @@ func (d *driverMock) DeleteImage(id string) error {
}
// GetInstanceIP returns the public or private IP corresponding to the given instance id.
func (d *driverMock) GetInstanceIP(id string) (string, error) {
func (d *driverMock) GetInstanceIP(ctx context.Context, id string) (string, error) {
if d.GetInstanceIPErr != nil {
return "", d.GetInstanceIPErr
}
@ -69,7 +73,7 @@ func (d *driverMock) GetInstanceIP(id string) (string, error) {
}
// TerminateInstance terminates a compute instance.
func (d *driverMock) TerminateInstance(id string) error {
func (d *driverMock) TerminateInstance(ctx context.Context, id string) error {
if d.TerminateInstanceErr != nil {
return d.TerminateInstanceErr
}
@ -81,12 +85,12 @@ func (d *driverMock) TerminateInstance(id string) error {
// WaitForImageCreation waits for a provisioning custom image to reach the
// "AVAILABLE" state.
func (d *driverMock) WaitForImageCreation(id string) error {
func (d *driverMock) WaitForImageCreation(ctx context.Context, id string) error {
return d.WaitForImageCreationErr
}
// WaitForInstanceState waits for an instance to reach the a given terminal
// state.
func (d *driverMock) WaitForInstanceState(id string, waitStates []string, terminalState string) error {
func (d *driverMock) WaitForInstanceState(ctx context.Context, id string, waitStates []string, terminalState string) error {
return d.WaitForInstanceStateErr
}

View file

@ -15,6 +15,7 @@ type driverOCI struct {
computeClient core.ComputeClient
vcnClient core.VirtualNetworkClient
cfg *Config
context context.Context
}
// NewDriverOCI Creates a new driverOCI with a connected compute client and a connected vcn client.
@ -37,22 +38,34 @@ func NewDriverOCI(cfg *Config) (Driver, error) {
}
// CreateInstance creates a new compute instance.
func (d *driverOCI) CreateInstance(publicKey string) (string, error) {
func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (string, error) {
metadata := map[string]string{
"ssh_authorized_keys": publicKey,
}
if d.cfg.Metadata != nil {
for key, value := range d.cfg.Metadata {
metadata[key] = value
}
}
if d.cfg.UserData != "" {
metadata["user_data"] = d.cfg.UserData
}
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: core.LaunchInstanceDetails{
instanceDetails := core.LaunchInstanceDetails{
AvailabilityDomain: &d.cfg.AvailabilityDomain,
CompartmentId: &d.cfg.CompartmentID,
ImageId: &d.cfg.BaseImageID,
Shape: &d.cfg.Shape,
SubnetId: &d.cfg.SubnetID,
Metadata: metadata,
}})
}
// When empty, the default display name is used.
if d.cfg.InstanceName != "" {
instanceDetails.DisplayName = &d.cfg.InstanceName
}
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{LaunchInstanceDetails: instanceDetails})
if err != nil {
return "", err
@ -62,8 +75,8 @@ func (d *driverOCI) CreateInstance(publicKey string) (string, error) {
}
// CreateImage creates a new custom image.
func (d *driverOCI) CreateImage(id string) (core.Image, error) {
res, err := d.computeClient.CreateImage(context.TODO(), core.CreateImageRequest{CreateImageDetails: core.CreateImageDetails{
func (d *driverOCI) CreateImage(ctx context.Context, id string) (core.Image, error) {
res, err := d.computeClient.CreateImage(ctx, core.CreateImageRequest{CreateImageDetails: core.CreateImageDetails{
CompartmentId: &d.cfg.CompartmentID,
InstanceId: &id,
DisplayName: &d.cfg.ImageName,
@ -77,14 +90,14 @@ func (d *driverOCI) CreateImage(id string) (core.Image, error) {
}
// DeleteImage deletes a custom image.
func (d *driverOCI) DeleteImage(id string) error {
_, err := d.computeClient.DeleteImage(context.TODO(), core.DeleteImageRequest{ImageId: &id})
func (d *driverOCI) DeleteImage(ctx context.Context, id string) error {
_, err := d.computeClient.DeleteImage(ctx, core.DeleteImageRequest{ImageId: &id})
return err
}
// GetInstanceIP returns the public or private IP corresponding to the given instance id.
func (d *driverOCI) GetInstanceIP(id string) (string, error) {
vnics, err := d.computeClient.ListVnicAttachments(context.TODO(), core.ListVnicAttachmentsRequest{
func (d *driverOCI) GetInstanceIP(ctx context.Context, id string) (string, error) {
vnics, err := d.computeClient.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{
InstanceId: &id,
CompartmentId: &d.cfg.CompartmentID,
})
@ -96,7 +109,7 @@ func (d *driverOCI) GetInstanceIP(id string) (string, error) {
return "", errors.New("instance has zero VNICs")
}
vnic, err := d.vcnClient.GetVnic(context.TODO(), core.GetVnicRequest{VnicId: vnics.Items[0].VnicId})
vnic, err := d.vcnClient.GetVnic(ctx, core.GetVnicRequest{VnicId: vnics.Items[0].VnicId})
if err != nil {
return "", fmt.Errorf("Error getting VNIC details: %s", err)
}
@ -112,8 +125,8 @@ func (d *driverOCI) GetInstanceIP(id string) (string, error) {
return *vnic.PublicIp, nil
}
func (d *driverOCI) GetInstanceInitialCredentials(id string) (string, string, error) {
credentials, err := d.computeClient.GetWindowsInstanceInitialCredentials(context.TODO(), core.GetWindowsInstanceInitialCredentialsRequest{
func (d *driverOCI) GetInstanceInitialCredentials(ctx context.Context, id string) (string, string, error) {
credentials, err := d.computeClient.GetWindowsInstanceInitialCredentials(ctx, core.GetWindowsInstanceInitialCredentialsRequest{
InstanceId: &id,
})
if err != nil {
@ -124,8 +137,8 @@ func (d *driverOCI) GetInstanceInitialCredentials(id string) (string, string, er
}
// TerminateInstance terminates a compute instance.
func (d *driverOCI) TerminateInstance(id string) error {
_, err := d.computeClient.TerminateInstance(context.TODO(), core.TerminateInstanceRequest{
func (d *driverOCI) TerminateInstance(ctx context.Context, id string) error {
_, err := d.computeClient.TerminateInstance(ctx, core.TerminateInstanceRequest{
InstanceId: &id,
})
return err
@ -133,10 +146,10 @@ func (d *driverOCI) TerminateInstance(id string) error {
// WaitForImageCreation waits for a provisioning custom image to reach the
// "AVAILABLE" state.
func (d *driverOCI) WaitForImageCreation(id string) error {
func (d *driverOCI) WaitForImageCreation(ctx context.Context, id string) error {
return waitForResourceToReachState(
func(string) (string, error) {
image, err := d.computeClient.GetImage(context.TODO(), core.GetImageRequest{ImageId: &id})
image, err := d.computeClient.GetImage(ctx, core.GetImageRequest{ImageId: &id})
if err != nil {
return "", err
}
@ -152,10 +165,10 @@ func (d *driverOCI) WaitForImageCreation(id string) error {
// WaitForInstanceState waits for an instance to reach the a given terminal
// state.
func (d *driverOCI) WaitForInstanceState(id string, waitStates []string, terminalState string) error {
func (d *driverOCI) WaitForInstanceState(ctx context.Context, id string, waitStates []string, terminalState string) error {
return waitForResourceToReachState(
func(string) (string, error) {
instance, err := d.computeClient.GetInstance(context.TODO(), core.GetInstanceRequest{InstanceId: &id})
instance, err := d.computeClient.GetInstance(ctx, core.GetInstanceRequest{InstanceId: &id})
if err != nil {
return "", err
}

View file

@ -10,7 +10,7 @@ import (
type stepCreateInstance struct{}
func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *stepCreateInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
var (
driver = state.Get("driver").(Driver)
ui = state.Get("ui").(packer.Ui)
@ -19,7 +19,7 @@ func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) mu
ui.Say("Creating instance...")
instanceID, err := driver.CreateInstance(publicKey)
instanceID, err := driver.CreateInstance(ctx, publicKey)
if err != nil {
err = fmt.Errorf("Problem creating instance: %s", err)
ui.Error(err.Error())
@ -33,7 +33,7 @@ func (s *stepCreateInstance) Run(_ context.Context, state multistep.StateBag) mu
ui.Say("Waiting for instance to enter 'RUNNING' state...")
if err = driver.WaitForInstanceState(instanceID, []string{"STARTING", "PROVISIONING"}, "RUNNING"); err != nil {
if err = driver.WaitForInstanceState(ctx, instanceID, []string{"STARTING", "PROVISIONING"}, "RUNNING"); err != nil {
err = fmt.Errorf("Error waiting for instance to start: %s", err)
ui.Error(err.Error())
state.Put("error", err)
@ -57,14 +57,14 @@ func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
ui.Say(fmt.Sprintf("Terminating instance (%s)...", id))
if err := driver.TerminateInstance(id); err != nil {
if err := driver.TerminateInstance(context.TODO(), id); err != nil {
err = fmt.Errorf("Error terminating instance. Please terminate manually: %s", err)
ui.Error(err.Error())
state.Put("error", err)
return
}
err := driver.WaitForInstanceState(id, []string{"TERMINATING"}, "TERMINATED")
err := driver.WaitForInstanceState(context.TODO(), id, []string{"TERMINATING"}, "TERMINATED")
if err != nil {
err = fmt.Errorf("Error terminating instance. Please terminate manually: %s", err)
ui.Error(err.Error())

View file

@ -17,7 +17,7 @@ type stepGetDefaultCredentials struct {
BuildName string
}
func (s *stepGetDefaultCredentials) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *stepGetDefaultCredentials) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
var (
driver = state.Get("driver").(*driverOCI)
ui = state.Get("ui").(packer.Ui)
@ -36,7 +36,7 @@ func (s *stepGetDefaultCredentials) Run(_ context.Context, state multistep.State
return multistep.ActionContinue
}
username, password, err := driver.GetInstanceInitialCredentials(id)
username, password, err := driver.GetInstanceInitialCredentials(ctx, id)
if err != nil {
err = fmt.Errorf("Error getting instance's credentials: %s", err)
ui.Error(err.Error())

View file

@ -10,7 +10,7 @@ import (
type stepImage struct{}
func (s *stepImage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
var (
driver = state.Get("driver").(Driver)
ui = state.Get("ui").(packer.Ui)
@ -19,7 +19,7 @@ func (s *stepImage) Run(_ context.Context, state multistep.StateBag) multistep.S
ui.Say("Creating image from instance...")
image, err := driver.CreateImage(instanceID)
image, err := driver.CreateImage(ctx, instanceID)
if err != nil {
err = fmt.Errorf("Error creating image from instance: %s", err)
ui.Error(err.Error())
@ -27,7 +27,7 @@ func (s *stepImage) Run(_ context.Context, state multistep.StateBag) multistep.S
return multistep.ActionHalt
}
err = driver.WaitForImageCreation(*image.Id)
err = driver.WaitForImageCreation(ctx, *image.Id)
if err != nil {
err = fmt.Errorf("Error waiting for image creation to finish: %s", err)
ui.Error(err.Error())

View file

@ -10,14 +10,14 @@ import (
type stepInstanceInfo struct{}
func (s *stepInstanceInfo) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
func (s *stepInstanceInfo) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
var (
driver = state.Get("driver").(Driver)
ui = state.Get("ui").(packer.Ui)
id = state.Get("instance_id").(string)
)
ip, err := driver.GetInstanceIP(id)
ip, err := driver.GetInstanceIP(ctx, id)
if err != nil {
err = fmt.Errorf("Error getting instance's IP: %s", err)
ui.Error(err.Error())

View file

@ -99,6 +99,7 @@ type Config struct {
Format string `mapstructure:"format"`
Headless bool `mapstructure:"headless"`
DiskImage bool `mapstructure:"disk_image"`
UseBackingFile bool `mapstructure:"use_backing_file"`
MachineType string `mapstructure:"machine_type"`
NetDevice string `mapstructure:"net_device"`
OutputDir string `mapstructure:"output_directory"`
@ -255,6 +256,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.DiskCompression = false
}
if b.config.UseBackingFile && !(b.config.DiskImage && b.config.Format == "qcow2") {
errs = packer.MultiErrorAppend(
errs, errors.New("use_backing_file can only be enabled for QCOW2 images and when disk_image is true"))
}
if _, ok := accels[b.config.Accelerator]; !ok {
errs = packer.MultiErrorAppend(
errs, errors.New("invalid accelerator, only 'kvm', 'tcg', 'xen', 'hax', 'hvf', or 'none' are allowed"))

View file

@ -224,6 +224,49 @@ func TestBuilderPrepare_Format(t *testing.T) {
}
}
func TestBuilderPrepare_UseBackingFile(t *testing.T) {
var b Builder
config := testConfig()
config["use_backing_file"] = true
// Bad: iso_url is not a disk_image
config["disk_image"] = false
config["format"] = "qcow2"
b = Builder{}
warns, err := b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Bad: format is not 'qcow2'
config["disk_image"] = true
config["format"] = "raw"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good: iso_url is a disk image and format is 'qcow2'
config["disk_image"] = true
config["format"] = "qcow2"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_FloppyFiles(t *testing.T) {
var b Builder
config := testConfig()

View file

@ -1,10 +1,15 @@
package qemu
import (
"fmt"
"net"
"os"
commonssh "github.com/hashicorp/packer/common/ssh"
"github.com/hashicorp/packer/communicator/ssh"
packerssh "github.com/hashicorp/packer/communicator/ssh"
"github.com/hashicorp/packer/helper/multistep"
gossh "golang.org/x/crypto/ssh"
"golang.org/x/crypto/ssh/agent"
)
func commHost(state multistep.StateBag) (string, error) {
@ -19,10 +24,29 @@ func commPort(state multistep.StateBag) (int, error) {
func sshConfig(state multistep.StateBag) (*gossh.ClientConfig, error) {
config := state.Get("config").(*Config)
auth := []gossh.AuthMethod{
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
ssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
var auth []gossh.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 = []gossh.AuthMethod{
gossh.PublicKeysCallback(agent.NewClient(sshAgent).Signers),
}
}
if config.Comm.SSHPassword != "" {
auth = append(auth,
gossh.Password(config.Comm.SSHPassword),
gossh.KeyboardInteractive(
packerssh.PasswordKeyboardInteractive(config.Comm.SSHPassword)),
)
}
if config.Comm.SSHPrivateKey != "" {

View file

@ -4,7 +4,9 @@ import (
"context"
"fmt"
"path/filepath"
"strings"
"github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
@ -46,11 +48,32 @@ func (s *stepConvertDisk) Run(_ context.Context, state multistep.StateBag) multi
)
ui.Say("Converting hard drive...")
if err := driver.QemuImg(command...); err != nil {
err := fmt.Errorf("Error converting hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
// Retry the conversion a few times in case it takes the qemu process a
// moment to release the lock
err := common.Retry(1, 10, 10, func(_ uint) (bool, error) {
if err := driver.QemuImg(command...); err != nil {
if strings.Contains(err.Error(), `Failed to get shared "write" lock`) {
ui.Say("Error getting file lock for conversion; retrying...")
return false, nil
}
err = fmt.Errorf("Error converting hard drive: %s", err)
return true, err
}
return true, nil
})
if err != nil {
if err == common.RetryExhaustedError {
err = fmt.Errorf("Exhausted retries for getting file lock: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} else {
err := fmt.Errorf("Error converting hard drive: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
if err := os.Rename(targetPath, sourcePath); err != nil {

View file

@ -28,7 +28,7 @@ func (s *stepCopyDisk) Run(_ context.Context, state multistep.StateBag) multiste
path,
}
if config.DiskImage == false {
if !config.DiskImage || config.UseBackingFile {
return multistep.ActionContinue
}

View file

@ -23,11 +23,19 @@ func (s *stepCreateDisk) Run(_ context.Context, state multistep.StateBag) multis
command := []string{
"create",
"-f", config.Format,
path,
fmt.Sprintf("%vM", config.DiskSize),
}
if config.DiskImage == true {
if config.UseBackingFile {
isoPath := state.Get("iso_path").(string)
command = append(command, "-b", isoPath)
}
command = append(command,
path,
fmt.Sprintf("%vM", config.DiskSize),
)
if config.DiskImage && !config.UseBackingFile {
return multistep.ActionContinue
}

View file

@ -41,6 +41,7 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
httpPort := state.Get("http_port").(uint)
ui := state.Get("ui").(packer.Ui)
vncPort := state.Get("vnc_port").(uint)
vncIP := state.Get("vnc_ip").(string)
if config.VNCConfig.DisableVNC {
log.Println("Skipping boot command step...")
@ -65,7 +66,7 @@ func (s *stepTypeBootCommand) Run(ctx context.Context, state multistep.StateBag)
// Connect to VNC
ui.Say("Connecting to VM via VNC")
nc, err := net.Dial("tcp", fmt.Sprintf("127.0.0.1:%d", vncPort))
nc, err := net.Dial("tcp", fmt.Sprintf("%s:%d", vncIP, vncPort))
if err != nil {
err := fmt.Errorf("Error connecting to VNC: %s", err)
state.Put("error", err)

View file

@ -29,6 +29,7 @@ type Config struct {
SnapshotName string `mapstructure:"snapshot_name"`
ImageName string `mapstructure:"image_name"`
ServerName string `mapstructure:"server_name"`
Bootscript string `mapstructure:"bootscript"`
UserAgent string
ctx interpolate.Context

View file

@ -20,9 +20,14 @@ func (s *stepCreateServer) Run(_ context.Context, state multistep.StateBag) mult
c := state.Get("config").(Config)
sshPubKey := state.Get("ssh_pubkey").(string)
tags := []string{}
var bootscript *string
ui.Say("Creating server...")
if c.Bootscript != "" {
bootscript = &c.Bootscript
}
if sshPubKey != "" {
tags = []string{fmt.Sprintf("AUTHORIZED_KEY=%s", strings.TrimSpace(sshPubKey))}
}
@ -33,6 +38,7 @@ func (s *stepCreateServer) Run(_ context.Context, state multistep.StateBag) mult
Organization: c.Organization,
CommercialType: c.CommercialType,
Tags: tags,
Bootscript: bootscript,
})
if err != nil {

View file

@ -94,7 +94,7 @@ func (s *StepDownloadGuestAdditions) Run(ctx context.Context, state multistep.St
} else {
ui.Error(err.Error())
url = fmt.Sprintf(
"http://download.virtualbox.org/virtualbox/%s/%s",
"https://download.virtualbox.org/virtualbox/%s/%s",
version,
additionsName)
}
@ -150,7 +150,7 @@ func (s *StepDownloadGuestAdditions) downloadAdditionsSHA256(ctx context.Context
// First things first, we get the list of checksums for the files available
// for this version.
checksumsUrl := fmt.Sprintf(
"http://download.virtualbox.org/virtualbox/%s/SHA256SUMS",
"https://download.virtualbox.org/virtualbox/%s/SHA256SUMS",
additionsVersion)
checksumsFile, err := ioutil.TempFile("", "packer")

View file

@ -3,6 +3,7 @@ package iso
import (
"context"
"fmt"
"path/filepath"
vboxcommon "github.com/hashicorp/packer/builder/virtualbox/common"
"github.com/hashicorp/packer/helper/multistep"
@ -34,6 +35,16 @@ func (s *stepAttachISO) Run(_ context.Context, state multistep.StateBag) multist
device = "0"
}
// If it's a symlink, resolve it to it's target.
resolvedIsoPath, err := filepath.EvalSymlinks(isoPath)
if err != nil {
err := fmt.Errorf("Error resolving symlink for ISO: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
isoPath = resolvedIsoPath
// Attach the disk to the controller
command := []string{
"storageattach", vmName,

View file

@ -23,7 +23,7 @@ type Driver interface {
// Clone clones the VMX and the disk to the destination path. The
// destination is a path to the VMX file. The disk will be copied
// to that same directory.
Clone(dst string, src string) error
Clone(dst string, src string, cloneType bool) error
// CompactDisk compacts a virtual disk.
CompactDisk(string) error
@ -358,7 +358,8 @@ func (d *VmwareDriver) GuestIP(state multistep.StateBag) (string, error) {
// open up the lease and read its contents
fh, err := os.Open(dhcpLeasesPath)
if err != nil {
return "", err
log.Printf("Error while reading DHCP lease path file %s: %s", dhcpLeasesPath, err.Error())
continue
}
defer fh.Close()

View file

@ -24,7 +24,7 @@ type Fusion5Driver struct {
SSHConfig *SSHConfig
}
func (d *Fusion5Driver) Clone(dst, src string) error {
func (d *Fusion5Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with Fusion 5. Please use Fusion 6+.")
}

View file

@ -18,11 +18,19 @@ type Fusion6Driver struct {
Fusion5Driver
}
func (d *Fusion6Driver) Clone(dst, src string) error {
func (d *Fusion6Driver) Clone(dst, src string, linked bool) error {
var cloneType string
if linked {
cloneType = "linked"
} else {
cloneType = "full"
}
cmd := exec.Command(d.vmrunPath(),
"-T", "fusion",
"clone", src, dst,
"full")
cloneType)
if _, _, err := runAndLog(cmd); err != nil {
if strings.Contains(err.Error(), "parameters was invalid") {
return fmt.Errorf(

View file

@ -13,6 +13,7 @@ type DriverMock struct {
CloneCalled bool
CloneDst string
CloneSrc string
Linked bool
CloneErr error
CompactDiskCalled bool
@ -107,10 +108,11 @@ func (m NetworkMapperMock) DeviceIntoName(device string) (string, error) {
return "", nil
}
func (d *DriverMock) Clone(dst string, src string) error {
func (d *DriverMock) Clone(dst string, src string, linked bool) error {
d.CloneCalled = true
d.CloneDst = dst
d.CloneSrc = src
d.Linked = linked
return d.CloneErr
}

View file

@ -25,7 +25,7 @@ type Player5Driver struct {
SSHConfig *SSHConfig
}
func (d *Player5Driver) Clone(dst, src string) error {
func (d *Player5Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with VMWare Player version 5. Please use VMWare Player version 6, or greater.")
}

View file

@ -13,13 +13,20 @@ type Player6Driver struct {
Player5Driver
}
func (d *Player6Driver) Clone(dst, src string) error {
func (d *Player6Driver) Clone(dst, src string, linked bool) error {
// TODO(rasa) check if running player+, not just player
var cloneType string
if linked {
cloneType = "linked"
} else {
cloneType = "full"
}
cmd := exec.Command(d.Player5Driver.VmrunPath,
"-T", "ws",
"clone", src, dst,
"full")
cloneType)
if _, _, err := runAndLog(cmd); err != nil {
return err

View file

@ -13,11 +13,19 @@ type Workstation10Driver struct {
Workstation9Driver
}
func (d *Workstation10Driver) Clone(dst, src string) error {
func (d *Workstation10Driver) Clone(dst, src string, linked bool) error {
var cloneType string
if linked {
cloneType = "linked"
} else {
cloneType = "full"
}
cmd := exec.Command(d.Workstation9Driver.VmrunPath,
"-T", "ws",
"clone", src, dst,
"full")
cloneType)
if _, _, err := runAndLog(cmd); err != nil {
return err

View file

@ -24,7 +24,7 @@ type Workstation9Driver struct {
SSHConfig *SSHConfig
}
func (d *Workstation9Driver) Clone(dst, src string) error {
func (d *Workstation9Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with VMware WS version 9. Please use VMware WS version 10, or greater.")
}

View file

@ -16,6 +16,9 @@ import (
//
// Uses:
// vmx_path string
//
// Produces:
// display_name string - Value of the displayName key set in the VMX file
type StepConfigureVMX struct {
CustomData map[string]string
SkipFloppy bool
@ -73,6 +76,19 @@ func (s *StepConfigureVMX) Run(_ context.Context, state multistep.StateBag) mult
return multistep.ActionHalt
}
// If the build is taking place on a remote ESX server, the displayName
// will be needed for discovery of the VM's IP address and for export
// of the VM. The displayName key should always be set in the VMX file,
// so error if we don't find it
if displayName, ok := vmxData["displayname"]; !ok { // Packer converts key names to lowercase!
err := fmt.Errorf("Error: Could not get value of displayName from VMX data")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
} else {
state.Put("display_name", displayName)
}
return multistep.ActionContinue
}

View file

@ -14,6 +14,9 @@ func testVMXFile(t *testing.T) string {
if err != nil {
t.Fatalf("err: %s", err)
}
// displayName must always be set
err = WriteVMX(tf.Name(), map[string]string{"displayName": "PackerBuild"})
tf.Close()
return tf.Name()
@ -132,12 +135,29 @@ func TestStepConfigureVMX_generatedAddresses(t *testing.T) {
vmxPath := testVMXFile(t)
defer os.Remove(vmxPath)
err := WriteVMX(vmxPath, map[string]string{
"foo": "bar",
"ethernet0.generatedAddress": "foo",
"ethernet1.generatedAddress": "foo",
"ethernet1.generatedAddressOffset": "foo",
})
additionalTestVmxData := []struct {
Key string
Value string
}{
{"foo", "bar"},
{"ethernet0.generatedaddress", "foo"},
{"ethernet1.generatedaddress", "foo"},
{"ethernet1.generatedaddressoffset", "foo"},
}
// Get any existing VMX data from the VMX file
vmxData, err := ReadVMX(vmxPath)
if err != nil {
t.Fatalf("err %s", err)
}
// Add the additional key/value pairs we need for this test to the existing VMX data
for _, data := range additionalTestVmxData {
vmxData[data.Key] = data.Value
}
// Recreate the VMX file so it includes all the data needed for this test
err = WriteVMX(vmxPath, vmxData)
if err != nil {
t.Fatalf("err: %s", err)
}
@ -157,7 +177,7 @@ func TestStepConfigureVMX_generatedAddresses(t *testing.T) {
if err != nil {
t.Fatalf("err: %s", err)
}
vmxData := ParseVMX(string(vmxContents))
vmxData = ParseVMX(string(vmxContents))
cases := []struct {
Key string
@ -180,5 +200,71 @@ func TestStepConfigureVMX_generatedAddresses(t *testing.T) {
}
}
}
}
// Should fail if the displayName key is not found in the VMX
func TestStepConfigureVMX_displayNameMissing(t *testing.T) {
state := testState(t)
step := new(StepConfigureVMX)
// testVMXFile adds displayName key/value pair to the VMX
vmxPath := testVMXFile(t)
defer os.Remove(vmxPath)
// Bad: Delete displayName from the VMX/Create an empty VMX file
err := WriteVMX(vmxPath, map[string]string{})
if err != nil {
t.Fatalf("err: %s", err)
}
state.Put("vmx_path", vmxPath)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v. Should halt when displayName key is missing from VMX", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should store error in state when displayName key is missing from VMX")
}
}
// Should store the value of displayName in the statebag
func TestStepConfigureVMX_displayNameStore(t *testing.T) {
state := testState(t)
step := new(StepConfigureVMX)
// testVMXFile adds displayName key/value pair to the VMX
vmxPath := testVMXFile(t)
defer os.Remove(vmxPath)
state.Put("vmx_path", vmxPath)
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
t.Fatalf("bad action: %#v", action)
}
if _, ok := state.GetOk("error"); ok {
t.Fatal("should NOT have error")
}
// The value of displayName must be stored in the statebag
if _, ok := state.GetOk("display_name"); !ok {
t.Fatalf("displayName should be stored in the statebag as 'display_name'")
}
}
func TestStepConfigureVMX_vmxPathBad(t *testing.T) {
state := testState(t)
step := new(StepConfigureVMX)
state.Put("vmx_path", "some_bad_path")
// Test the run
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
t.Fatalf("bad action: %#v. Should halt when vmxPath is bad", action)
}
if _, ok := state.GetOk("error"); !ok {
t.Fatal("should store error in state when vmxPath is bad")
}
}

View file

@ -221,6 +221,11 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
fmt.Errorf("format must be one of ova, ovf, or vmx"))
}
if b.config.RemoteType == "esx5" && b.config.SkipExport != true && b.config.RemotePassword == "" {
errs = packer.MultiErrorAppend(errs,
fmt.Errorf("exporting the vm (with ovftool) requires that you set a value for remote_password"))
}
// Warnings
if b.config.ShutdownCommand == "" {
warnings = append(warnings,

View file

@ -145,6 +145,7 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
config["format"] = "ovf"
config["remote_host"] = "foobar.example.com"
config["remote_password"] = "supersecret"
// Bad
config["remote_type"] = "foobar"
warns, err := b.Prepare(config)
@ -155,9 +156,10 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
t.Fatal("should have error")
}
config["remote_host"] = ""
config["remote_type"] = ""
config["remote_type"] = "esx5"
// Bad
config["remote_host"] = ""
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
t.Fatalf("bad: %#v", warns)
@ -167,8 +169,10 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
}
// Good
config["remote_type"] = "esx5"
config["remote_host"] = "foobar.example.com"
config["remote_type"] = ""
config["remote_host"] = ""
config["remote_password"] = ""
config["remote_private_key_file"] = ""
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
@ -181,6 +185,7 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
// Good
config["remote_type"] = "esx5"
config["remote_host"] = "foobar.example.com"
config["remote_password"] = "supersecret"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) > 0 {
@ -191,6 +196,34 @@ func TestBuilderPrepare_RemoteType(t *testing.T) {
}
}
func TestBuilderPrepare_RemoteExport(t *testing.T) {
var b Builder
config := testConfig()
config["remote_type"] = "esx5"
config["remote_host"] = "foobar.example.com"
// Bad
config["remote_password"] = ""
warns, err := b.Prepare(config)
if len(warns) != 0 {
t.Fatalf("bad: %#v", warns)
}
if err == nil {
t.Fatal("should have error")
}
// Good
config["remote_password"] = "supersecret"
b = Builder{}
warns, err = b.Prepare(config)
if len(warns) != 0 {
t.Fatalf("err: %s", err)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
}
func TestBuilderPrepare_Format(t *testing.T) {
var b Builder
config := testConfig()

View file

@ -41,7 +41,7 @@ type ESX5Driver struct {
vmId string
}
func (d *ESX5Driver) Clone(dst, src string) error {
func (d *ESX5Driver) Clone(dst, src string, linked bool) error {
return errors.New("Cloning is not supported with the ESX driver.")
}
@ -389,7 +389,13 @@ func (d *ESX5Driver) CommHost(state multistep.StateBag) (string, error) {
return "", err
}
record, err := r.find("Name", config.VMName)
// The value in the Name field returned by 'esxcli network vm list'
// corresponds directly to the value of displayName set in the VMX file
var displayName string
if v, ok := state.GetOk("display_name"); ok {
displayName = v.(string)
}
record, err := r.find("Name", displayName)
if err != nil {
return "", err
}

View file

@ -14,13 +14,17 @@ import (
"github.com/hashicorp/packer/packer"
)
// This step exports a VM built on ESXi using ovftool
//
// Uses:
// display_name string
type StepExport struct {
Format string
SkipExport bool
OutputDir string
}
func (s *StepExport) generateArgs(c *Config, hidePassword bool) []string {
func (s *StepExport) generateArgs(c *Config, displayName string, hidePassword bool) []string {
password := url.QueryEscape(c.RemotePassword)
if hidePassword {
password = "****"
@ -29,7 +33,7 @@ func (s *StepExport) generateArgs(c *Config, hidePassword bool) []string {
"--noSSLVerify=true",
"--skipManifestCheck",
"-tt=" + s.Format,
"vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + c.VMName,
"vi://" + c.RemoteUser + ":" + password + "@" + c.RemoteHost + "/" + displayName,
s.OutputDir,
}
return append(c.OVFToolOptions, args...)
@ -72,9 +76,13 @@ func (s *StepExport) Run(_ context.Context, state multistep.StateBag) multistep.
}
ui.Say("Exporting virtual machine...")
ui.Message(fmt.Sprintf("Executing: %s %s", ovftool, strings.Join(s.generateArgs(c, true), " ")))
var displayName string
if v, ok := state.GetOk("display_name"); ok {
displayName = v.(string)
}
ui.Message(fmt.Sprintf("Executing: %s %s", ovftool, strings.Join(s.generateArgs(c, displayName, true), " ")))
var out bytes.Buffer
cmd := exec.Command(ovftool, s.generateArgs(c, false)...)
cmd := exec.Command(ovftool, s.generateArgs(c, displayName, false)...)
cmd.Stdout = &out
if err := cmd.Run(); err != nil {
err := fmt.Errorf("Error exporting virtual machine: %s\n%s\n", err, out.String())

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