Compare commits
213 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| a000e24dc7 | |||
| 6941f72b21 | |||
| 4540285b15 | |||
| a14d64f0ed | |||
| e7b2751d63 | |||
| 19bafd8da8 | |||
| f717edd644 | |||
| ca1388f7bc | |||
| 62fbf8fef8 | |||
| c16c467e63 | |||
| 6805d62b44 | |||
| caa4fce2d3 | |||
| d08019c1a7 | |||
| 6f1675ab6b | |||
| ec8ba97c11 | |||
| c186d2dc3e | |||
| 06643423b1 | |||
| e50a15c4ee | |||
| 99a7ae8792 | |||
| 20541a7eda | |||
| 2c03c1a64e | |||
| f626790edd | |||
| 9865dd0161 | |||
| c680fe3909 | |||
| 95682f6dfc | |||
| 869c0b94ff | |||
| e6960e3e04 | |||
| 2746139591 | |||
| f536e351fa | |||
| 53890e9de3 | |||
| 56a309941b | |||
| 286c36b3c4 | |||
| fa2347e711 | |||
| 5e841befee | |||
| e43d08a3fc | |||
| 2e760ee57b | |||
| 9bc3702adc | |||
| e7140cf405 | |||
| 99dd3ccec5 | |||
| d6fde7dcb1 | |||
| da1de12b78 | |||
| 5eb97908c2 | |||
| aa47487960 | |||
| 1948350d20 | |||
| e75d3c1fbb | |||
| 7895df8c8f | |||
| 8a194a3750 | |||
| dbfe8e21e1 | |||
| f5c7c6ea90 | |||
| e589bf2724 | |||
| 8311b3508c | |||
| 894ca6deca | |||
| 32f1092608 | |||
| c365dd2f9f | |||
| d6d54cc406 | |||
| 43dd2bd2c9 | |||
| 088665840f | |||
| 14517e6fa5 | |||
| 9527f60884 | |||
| 788fe84053 | |||
| f56601fd64 | |||
| 4b2d1efb13 | |||
| da70359949 | |||
| 3a19127f53 | |||
| 303a003d2f | |||
| 5d7dc6e1c9 | |||
| 012b534873 | |||
| 701b867a95 | |||
| e550c4503e | |||
| d3fff7d145 | |||
| 4513a2509e | |||
| 2b797bb1ed | |||
| c7e932ee6e | |||
| a7d3638146 | |||
| 5a1073a980 | |||
| 99a7116939 | |||
| dec7f3f2ef | |||
| b2157b87b9 | |||
| f4f433d0d8 | |||
| d9e80803c9 | |||
| 9810766373 | |||
| 385ca4cc97 | |||
| 0306759840 | |||
| 50e2535bd8 | |||
| f197c2b5f0 | |||
| 374f2fb647 | |||
| cd4fb50cb7 | |||
| 4522db5d7d | |||
| 16df9a2c69 | |||
| 6fd17791e6 | |||
| db01abc008 | |||
| 3375c9e4fd | |||
| 53b13869b3 | |||
| 24b12993b7 | |||
| ffe63b8bd8 | |||
| 58a48ed171 | |||
| 2cf112c0ba | |||
| cc0f91b41b | |||
| e2ce81ce5e | |||
| 1e27feb6c6 | |||
| cfb53005ff | |||
| ba900fc2e8 | |||
| 148394a264 | |||
| 45ad452c3b | |||
| 415fb2c935 | |||
| aa383885c5 | |||
| 77dd02c332 | |||
| b059cce542 | |||
| 5d7586cc59 | |||
| 0b8bd1d7b7 | |||
| 2b5282b3d8 | |||
| 4524b13911 | |||
| 32ab55c79f | |||
| 2b2903c4eb | |||
| de1cabb1f3 | |||
| e64aec3e57 | |||
| fff968eb5a | |||
| 21b1d1f00e | |||
| 96f8b45add | |||
| 32216f5707 | |||
| 29ede35b28 | |||
| 6e99c468d4 | |||
| db60498f4f | |||
| cd7e0403fd | |||
| fd21277907 | |||
| 28a8293a22 | |||
| 7ad307e95a | |||
| 844e355ed3 | |||
| 489a568741 | |||
| 0208548082 | |||
| 5d9a2b63ff | |||
| 629ec33aa8 | |||
| a73ec1deb7 | |||
| c84d2aeffc | |||
| 1d0ceec7af | |||
| 513e4a2a3a | |||
| 9bf7d7b81b | |||
| 58960a8790 | |||
| 86a9d4fa09 | |||
| 29812ae9b7 | |||
| 258e247cf6 | |||
| 8d8edc998a | |||
| 9da7b5db30 | |||
| 6ef73b88be | |||
| 775555944e | |||
| 9b6e4ac92c | |||
| 885ea52e56 | |||
| 0a950a7cdc | |||
| c0f39e5dca | |||
| de80940027 | |||
| fd0e642195 | |||
| 154adee43c | |||
| 40fccde1b0 | |||
| a1a7ff20c2 | |||
| f19dcf15e1 | |||
| 53bd115e3e | |||
| 3b4bf9f206 | |||
| fc61f8cb1d | |||
| a3407e5e3d | |||
| deaec36040 | |||
| 015933b29f | |||
| a6d4e326a7 | |||
| 51fb48c5cd | |||
| bda45c1b53 | |||
| 4188e07fc2 | |||
| 2946d14edf | |||
| 78e891c2b1 | |||
| 507135ec84 | |||
| 40d6c46ed3 | |||
| 165bac93d0 | |||
| 3242498928 | |||
| c193048b3b | |||
| 9f84f2c363 | |||
| f0592e8c70 | |||
| 24d60b61c7 | |||
| 91af560d26 | |||
| 989a9a8d63 | |||
| 0dfaacc09d | |||
| 80e37446e9 | |||
| 5f11d86279 | |||
| 7f4132537a | |||
| cfca3d8f66 | |||
| 6fb454ef0c | |||
| 4e854f6103 | |||
| 69902bb0f0 | |||
| 08f724e6b7 | |||
| c2ccd19b15 | |||
| 1fc4703b97 | |||
| 5c26225b86 | |||
| 9b01b38a87 | |||
| 9775c00881 | |||
| 70282fedee | |||
| 6eceacf11f | |||
| e22eb3aa10 | |||
| 02987f6c97 | |||
| c163d53437 | |||
| eb5559817b | |||
| 2789162fec | |||
| 6090b6ecdf | |||
| 2bec873de5 | |||
| 428bc4d745 | |||
| ba5d7a9d72 | |||
| a6f3bb3bb2 | |||
| 1fadd03837 | |||
| a84f26bfdf | |||
| 24c4b5334b | |||
| 69f0049a44 | |||
| 3416f0760c | |||
| e5e97f3cc1 | |||
| 552298b683 | |||
| bb3ff62623 | |||
| 608d874b45 | |||
| 45a16fceac |
+24
@@ -0,0 +1,24 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- tip
|
||||
|
||||
install: make deps
|
||||
script:
|
||||
- go test ./...
|
||||
- go test -race ./...
|
||||
|
||||
notifications:
|
||||
flowdock:
|
||||
secure: fZrcf9rlh2IrQrlch1sHkn3YI7SKvjGnAl/zyV5D6NROe1Bbr6d3QRMuCXWWdhJHzjKmXk5rIzbqJhUc0PNF7YjxGNKSzqWMQ56KcvN1k8DzlqxpqkcA3Jbs6fXCWo2fssRtZ7hj/wOP1f5n6cc7kzHDt9dgaYJ6nO2fqNPJiTc=
|
||||
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#packer-tool"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
+129
@@ -1,3 +1,132 @@
|
||||
## 0.3.5 (August 28, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **NEW BUILDER:** `openstack`. You can now build on OpenStack. [GH-155]
|
||||
* **NEW PROVISIONER:** `chef-solo`. You can now provision with Chef
|
||||
using `chef-solo` from local cookbooks.
|
||||
* builder/amazon: Copy AMI to multiple regions with `ami_regions`. [GH-322]
|
||||
* builder/virtualbox,vmware: Can now use SSH keys as an auth mechanism for
|
||||
SSH using `ssh_key_path`. [GH-70]
|
||||
* builder/virtualbox,vmware: Support SHA512 as a checksum type. [GH-356]
|
||||
* builder/vmware: The root hard drive type can now be specified with
|
||||
"disk_type_id" for advanced users. [GH-328]
|
||||
* provisioner/salt-masterless: Ability to specfy a minion config. [GH-264]
|
||||
* provisioner/salt-masterless: Ability to upload pillars. [GH-353]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: Output message when Ctrl-C received that we're cleaning up. [GH-338]
|
||||
* builder/amazon: Tagging now works with all amazon builder types.
|
||||
* builder/vmware: Option `ssh_skip_request_pty` for not requesting a PTY
|
||||
for the SSH connection. [GH-270]
|
||||
* builder/vmware: Specify a `vmx_template_path` in order to customize
|
||||
the generated VMX. [GH-270]
|
||||
* command/build: Machine-readable output now contains build errors, if any.
|
||||
* command/build: An "end" sentinel is outputted in machine-readable output
|
||||
for artifact listing so it is easier to know when it is over.
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Fixed a couple cases where a double ctrl-C could panic.
|
||||
* core: Template validation fails if an override is specified for a
|
||||
non-existent builder. [GH-336]
|
||||
* core: The SSH connection is heartbeated so that drops can be
|
||||
detected. [GH-200]
|
||||
* builder/amazon/instance: Remove check for ec2-ami-tools because it
|
||||
didn't allow absolute paths to work properly. [GH-330]
|
||||
* builder/digitalocean: Send a soft shutdown request so that files
|
||||
are properly synced before shutdown. [GH-332]
|
||||
* command/build,command/validate: If a non-existent build is specified to
|
||||
'-only' or '-except', it is now an error. [GH-326]
|
||||
* post-processor/vagrant: Setting OutputPath with a timestamp now
|
||||
always works properly. [GH-324]
|
||||
* post-processor/vagrant: VirtualBox OVA formats now turn into
|
||||
Vagrant boxes properly. [GH-331]
|
||||
* provisioner/shell: Retry upload if start command fails, making reboot
|
||||
handling much more robust.
|
||||
|
||||
## 0.3.4 (August 21, 2013)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* post-processor/vagrant: the file being compressed will be shown
|
||||
in the UI [GH-314]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Avoid panics when double-interrupting Packer.
|
||||
* provisioner/shell: Retry shell script uploads, making reboots more
|
||||
robust if they happen to fail in this stage. [GH-282]
|
||||
|
||||
## 0.3.3 (August 19, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* builder/virtualbox: support exporting in OVA format. [GH-309]
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: All HTTP downloads across Packer now support the standard
|
||||
proxy environmental variables (`HTTP_PROXY`, `NO_PROXY`, etc.) [GH-252]
|
||||
* builder/amazon: API requests will use HTTP proxy if specified by
|
||||
enviromental variables.
|
||||
* builder/digitalocean: API requests will use HTTP proxy if specified
|
||||
by environmental variables.
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: TCP connection between plugin processes will keep-alive. [GH-312]
|
||||
* core: No more "unused key keep_input_artifact" for post processors [GH-310]
|
||||
* post-processor/vagrant: `output_path` templates now work again.
|
||||
|
||||
## 0.3.2 (August 18, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* New command: `packer inspect`. This command tells you the components of
|
||||
a template. It respects the `-machine-readable` flag as well so you can
|
||||
parse out components of a template.
|
||||
* Packer will detect its own crashes (always a bug) and save a "crash.log"
|
||||
file.
|
||||
* builder/virtualbox: You may now specify multiple URLs for an ISO
|
||||
using "iso_url" in a template. The URLs will be tried in order.
|
||||
* builder/vmware: You may now specify multiple URLs for an ISO
|
||||
using "iso_url" in a template. The URLs will be tried in order.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: built with Go 1.1.2
|
||||
* core: packer help output now loads much faster.
|
||||
* builder/virtualbox: guest_additions_url can now use the `Version`
|
||||
variable to get the VirtualBox version. [GH-272]
|
||||
* builder/virtualbox: Do not check for VirtualBox as part of template
|
||||
validation; only check at execution.
|
||||
* builder/vmware: Do not check for VMware as part of template validation;
|
||||
only check at execution.
|
||||
* command/build: A path of "-" will read the template from stdin.
|
||||
* builder/amazon: add block device mappings [GH-90]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* windows: file URLs are easier to get right as Packer
|
||||
has better parsing and error handling for Windows file paths. [GH-284]
|
||||
* builder/amazon/all: Modifying more than one AMI attribute type no longer
|
||||
crashes.
|
||||
* builder/amazon-instance: send IAM instance profile data. [GH-294]
|
||||
* builder/digitalocean: API request parameters are properly URL
|
||||
encoded. [GH-281]
|
||||
* builder/virtualbox: dowload progress won't be shown until download
|
||||
actually starts. [GH-288]
|
||||
* builder/virtualbox: floppy files names of 13 characters are now properly
|
||||
written to the FAT12 filesystem. [GH-285]
|
||||
* builder/vmware: dowload progress won't be shown until download
|
||||
actually starts. [GH-288]
|
||||
* builder/vmware: interrupt works while typing commands over VNC.
|
||||
* builder/virtualbox: floppy files names of 13 characters are now properly
|
||||
written to the FAT12 filesystem. [GH-285]
|
||||
* post-processor/vagrant: Process user variables. [GH-295]
|
||||
|
||||
## 0.3.1 (August 12, 2013)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
@@ -3,22 +3,24 @@ OK_COLOR=\033[32;01m
|
||||
ERROR_COLOR=\033[31;01m
|
||||
WARN_COLOR=\033[33;01m
|
||||
|
||||
all:
|
||||
all: deps
|
||||
@mkdir -p bin/
|
||||
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
|
||||
@go get -d -v ./...
|
||||
@echo "$(OK_COLOR)==> Building$(NO_COLOR)"
|
||||
@./scripts/build.sh
|
||||
|
||||
deps:
|
||||
@echo "$(OK_COLOR)==> Installing dependencies$(NO_COLOR)"
|
||||
@go get -d -v ./...
|
||||
@go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d
|
||||
|
||||
clean:
|
||||
@rm -rf bin/ local/ pkg/ src/ website/.sass-cache website/build
|
||||
|
||||
format:
|
||||
go fmt ./...
|
||||
|
||||
test:
|
||||
test: deps
|
||||
@echo "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
|
||||
@go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d
|
||||
go test ./...
|
||||
|
||||
.PHONY: all format test
|
||||
.PHONY: all deps format test
|
||||
|
||||
@@ -34,7 +34,7 @@ type Config struct {
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
UnmountCommand string `mapstructure:"unmount_command"`
|
||||
|
||||
tpl *common.Template
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -48,7 +48,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -194,6 +194,13 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -70,6 +70,38 @@ func (c *Communicator) Upload(dst string, r io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
walkFn := func(fullPath string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path, err := filepath.Rel(src, fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, e := range exclude {
|
||||
if e == path {
|
||||
log.Printf("Skipping excluded file: %s", path)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
dstPath := filepath.Join(dst, path)
|
||||
f, err := os.Open(fullPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return c.Upload(dstPath, f)
|
||||
}
|
||||
|
||||
log.Printf("Uploading directory '%s' to '%s'", src, dst)
|
||||
return filepath.Walk(src, walkFn)
|
||||
}
|
||||
|
||||
func (c *Communicator) Download(src string, w io.Writer) error {
|
||||
src = filepath.Join(c.Chroot, src)
|
||||
log.Printf("Downloading from chroot dir: %s", src)
|
||||
|
||||
@@ -3,7 +3,7 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
@@ -37,10 +37,10 @@ func (c *AccessConfig) Region() (aws.Region, error) {
|
||||
return aws.Regions[region], nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare(t *common.Template) []error {
|
||||
func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = common.NewTemplate()
|
||||
t, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
@@ -2,22 +2,25 @@ package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// AMIConfig is for common configuration related to creating AMIs.
|
||||
type AMIConfig struct {
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
AMIDescription string `mapstructure:"ami_description"`
|
||||
AMIUsers []string `mapstructure:"ami_users"`
|
||||
AMIGroups []string `mapstructure:"ami_groups"`
|
||||
AMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
AMIDescription string `mapstructure:"ami_description"`
|
||||
AMIUsers []string `mapstructure:"ami_users"`
|
||||
AMIGroups []string `mapstructure:"ami_groups"`
|
||||
AMIProductCodes []string `mapstructure:"ami_product_codes"`
|
||||
AMIRegions []string `mapstructure:"ami_regions"`
|
||||
AMITags map[string]string `mapstructure:"tags"`
|
||||
}
|
||||
|
||||
func (c *AMIConfig) Prepare(t *common.Template) []error {
|
||||
func (c *AMIConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = common.NewTemplate()
|
||||
t, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
@@ -42,6 +45,7 @@ func (c *AMIConfig) Prepare(t *common.Template) []error {
|
||||
"ami_users": c.AMIUsers,
|
||||
"ami_groups": c.AMIGroups,
|
||||
"ami_product_codes": c.AMIProductCodes,
|
||||
"ami_regions": c.AMIRegions,
|
||||
}
|
||||
|
||||
for n, slice := range sliceTemplates {
|
||||
@@ -59,6 +63,52 @@ func (c *AMIConfig) Prepare(t *common.Template) []error {
|
||||
errs = append(errs, fmt.Errorf("ami_name must be specified"))
|
||||
}
|
||||
|
||||
if len(c.AMIRegions) > 0 {
|
||||
regionSet := make(map[string]struct{})
|
||||
regions := make([]string, 0, len(c.AMIRegions))
|
||||
|
||||
for _, region := range c.AMIRegions {
|
||||
// If we already saw the region, then don't look again
|
||||
if _, ok := regionSet[region]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Mark that we saw the region
|
||||
regionSet[region] = struct{}{}
|
||||
|
||||
// Verify the region is real
|
||||
if _, ok := aws.Regions[region]; !ok {
|
||||
errs = append(errs, fmt.Errorf("Unknown region: %s", region))
|
||||
continue
|
||||
}
|
||||
|
||||
regions = append(regions, region)
|
||||
}
|
||||
|
||||
c.AMIRegions = regions
|
||||
}
|
||||
|
||||
newTags := make(map[string]string)
|
||||
for k, v := range c.AMITags {
|
||||
k, err := t.Process(k, nil)
|
||||
if err != nil {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("Error processing tag key %s: %s", k, err))
|
||||
continue
|
||||
}
|
||||
|
||||
v, err := t.Process(v, nil)
|
||||
if err != nil {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("Error processing tag value '%s': %s", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
newTags[k] = v
|
||||
}
|
||||
|
||||
c.AMITags = newTags
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -10,7 +11,7 @@ func testAMIConfig() *AMIConfig {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_Region(t *testing.T) {
|
||||
func TestAMIConfigPrepare_name(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
@@ -21,3 +22,26 @@ func TestAMIConfigPrepare_Region(t *testing.T) {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_regions(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
c.AMIRegions = nil
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.AMIRegions = []string{"foo"}
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
c.AMIRegions = []string{"us-east-1", "us-west-1", "us-east-1"}
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("bad: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"us-east-1", "us-west-1"}
|
||||
if !reflect.DeepEqual(c.AMIRegions, expected) {
|
||||
t.Fatalf("bad: %#v", c.AMIRegions)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
)
|
||||
|
||||
// BlockDevice
|
||||
type BlockDevice struct {
|
||||
DeviceName string `mapstructure:"device_name"`
|
||||
VirtualName string `mapstructure:"virtual_name"`
|
||||
SnapshotId string `mapstructure:"snapshot_id"`
|
||||
VolumeType string `mapstructure:"volume_type"`
|
||||
VolumeSize int64 `mapstructure:"volume_size"`
|
||||
DeleteOnTermination bool `mapstructure:"delete_on_termination"`
|
||||
IOPS int64 `mapstructure:"iops"`
|
||||
}
|
||||
|
||||
type BlockDevices struct {
|
||||
AMIMappings []BlockDevice `mapstructure:"ami_block_device_mappings,squash"`
|
||||
LaunchMappings []BlockDevice `mapstructure:"launch_block_device_mappings,squash"`
|
||||
}
|
||||
|
||||
func buildBlockDevices(b []BlockDevice) []ec2.BlockDeviceMapping {
|
||||
var blockDevices []ec2.BlockDeviceMapping
|
||||
|
||||
for _, blockDevice := range b {
|
||||
blockDevices = append(blockDevices, ec2.BlockDeviceMapping{
|
||||
DeviceName: blockDevice.DeviceName,
|
||||
VirtualName: blockDevice.VirtualName,
|
||||
SnapshotId: blockDevice.SnapshotId,
|
||||
VolumeType: blockDevice.VolumeType,
|
||||
VolumeSize: blockDevice.VolumeSize,
|
||||
DeleteOnTermination: blockDevice.DeleteOnTermination,
|
||||
IOPS: blockDevice.IOPS,
|
||||
})
|
||||
}
|
||||
return blockDevices
|
||||
}
|
||||
|
||||
func (b *BlockDevices) BuildAMIDevices() []ec2.BlockDeviceMapping {
|
||||
return buildBlockDevices(b.AMIMappings)
|
||||
}
|
||||
|
||||
func (b *BlockDevices) BuildLaunchDevices() []ec2.BlockDeviceMapping {
|
||||
return buildBlockDevices(b.LaunchMappings)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBlockDevice(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
ec2Mapping := []ec2.BlockDeviceMapping{
|
||||
ec2.BlockDeviceMapping{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
SnapshotId: "snap-1234",
|
||||
VolumeType: "standard",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
IOPS: 1000,
|
||||
},
|
||||
}
|
||||
|
||||
blockDevice := BlockDevice{
|
||||
DeviceName: "/dev/sdb",
|
||||
VirtualName: "ephemeral0",
|
||||
SnapshotId: "snap-1234",
|
||||
VolumeType: "standard",
|
||||
VolumeSize: 8,
|
||||
DeleteOnTermination: true,
|
||||
IOPS: 1000,
|
||||
}
|
||||
|
||||
blockDevices := BlockDevices{
|
||||
AMIMappings: []BlockDevice{blockDevice},
|
||||
LaunchMappings: []BlockDevice{blockDevice},
|
||||
}
|
||||
|
||||
assert.Equal(ec2Mapping, blockDevices.BuildAMIDevices(), "should match output")
|
||||
assert.Equal(ec2Mapping, blockDevices.BuildLaunchDevices(), "should match output")
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package common
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
@@ -27,10 +27,10 @@ type RunConfig struct {
|
||||
sshTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare(t *common.Template) []error {
|
||||
func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = common.NewTemplate()
|
||||
t, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepAMIRegionCopy struct {
|
||||
Regions []string
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (s *StepAMIRegionCopy) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
amis := state["amis"].(map[string]string)
|
||||
ami := amis[ec2conn.Region.Name]
|
||||
|
||||
if len(s.Regions) == 0 {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Copying AMI (%s) to other regions...", ami))
|
||||
for _, region := range s.Regions {
|
||||
ui.Message(fmt.Sprintf("Copying to: %s", region))
|
||||
|
||||
// Connect to the region where the AMI will be copied to
|
||||
regionconn := ec2.New(ec2conn.Auth, aws.Regions[region])
|
||||
resp, err := regionconn.CopyImage(&ec2.CopyImage{
|
||||
SourceRegion: ec2conn.Region.Name,
|
||||
SourceImageId: ami,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error Copying AMI (%s) to region (%s): %s", ami, region, err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Waiting for AMI (%s) in region (%s) to become ready...", resp.ImageId, region))
|
||||
if err := WaitForAMI(regionconn, resp.ImageId); err != nil {
|
||||
err := fmt.Errorf("Error waiting for AMI (%s) in region (%s): %s", resp.ImageId, region, err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Need to re-apply Tags since they are not copied with the AMI
|
||||
if len(s.Tags) > 0 {
|
||||
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", resp.ImageId))
|
||||
|
||||
var ec2Tags []ec2.Tag
|
||||
for key, value := range s.Tags {
|
||||
ui.Message(fmt.Sprintf("Adding tag: \"%s\": \"%s\"", key, value))
|
||||
ec2Tags = append(ec2Tags, ec2.Tag{key, value})
|
||||
}
|
||||
|
||||
_, err := regionconn.CreateTags([]string{resp.ImageId}, ec2Tags)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to AMI (%s): %s", resp.ImageId, err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
amis[region] = resp.ImageId
|
||||
}
|
||||
|
||||
state["amis"] = amis
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepAMIRegionCopy) Cleanup(state map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -31,20 +31,44 @@ func (s *StepModifyAMIAttributes) Run(state map[string]interface{}) multistep.St
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
options := &ec2.ModifyImageAttribute{
|
||||
Description: s.Description,
|
||||
AddUsers: s.Users,
|
||||
AddGroups: s.Groups,
|
||||
ProductCodes: s.ProductCodes,
|
||||
// Construct the modify image attribute requests we're going to make.
|
||||
// We need to make each separately since the EC2 API only allows changing
|
||||
// one type at a kind currently.
|
||||
options := make(map[string]*ec2.ModifyImageAttribute)
|
||||
if s.Description != "" {
|
||||
options["description"] = &ec2.ModifyImageAttribute{
|
||||
Description: s.Description,
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Groups) > 0 {
|
||||
options["groups"] = &ec2.ModifyImageAttribute{
|
||||
AddGroups: s.Groups,
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.Users) > 0 {
|
||||
options["users"] = &ec2.ModifyImageAttribute{
|
||||
AddUsers: s.Users,
|
||||
}
|
||||
}
|
||||
|
||||
if len(s.ProductCodes) > 0 {
|
||||
options["product codes"] = &ec2.ModifyImageAttribute{
|
||||
ProductCodes: s.ProductCodes,
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("Modifying AMI attributes...")
|
||||
_, err := ec2conn.ModifyImageAttribute(ami, options)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error modify AMI attributes: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
for name, opts := range options {
|
||||
ui.Message(fmt.Sprintf("Modifying: %s", name))
|
||||
_, err := ec2conn.ModifyImageAttribute(ami, opts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error modify AMI attributes: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -17,6 +17,7 @@ type StepRunSourceInstance struct {
|
||||
SourceAMI string
|
||||
IamInstanceProfile string
|
||||
SubnetId string
|
||||
BlockDevices BlockDevices
|
||||
|
||||
instance *ec2.Instance
|
||||
}
|
||||
@@ -48,6 +49,7 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
|
||||
IamInstanceProfile: s.IamInstanceProfile,
|
||||
SubnetId: s.SubnetId,
|
||||
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
@@ -90,7 +92,6 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
StepState: state,
|
||||
}
|
||||
latestInstance, err := WaitForState(&stateChange)
|
||||
s.instance = latestInstance.(*ec2.Instance)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for instance (%s) to become ready: %s", s.instance.InstanceId, err)
|
||||
state["error"] = err
|
||||
@@ -98,6 +99,7 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.instance = latestInstance.(*ec2.Instance)
|
||||
state["instance"] = s.instance
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
@@ -22,12 +21,10 @@ type config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.BlockDevices `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
// Tags for the AMI
|
||||
Tags map[string]string
|
||||
|
||||
tpl *common.Template
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -41,7 +38,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -53,28 +50,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
// Accumulate any errors
|
||||
newTags := make(map[string]string)
|
||||
for k, v := range b.config.Tags {
|
||||
k, err = b.config.tpl.Process(k, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing tag key %s: %s", k, err))
|
||||
continue
|
||||
}
|
||||
|
||||
v, err = b.config.tpl.Process(v, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing tag value '%s': %s", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
newTags[k] = v
|
||||
}
|
||||
|
||||
b.config.Tags = newTags
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -119,6 +94,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
SubnetId: b.config.SubnetId,
|
||||
BlockDevices: b.config.BlockDevices,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
|
||||
@@ -128,12 +104,18 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepCreateTags{Tags: b.config.Tags},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -19,8 +19,9 @@ func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
|
||||
createOpts := &ec2.CreateImage{
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: config.AMIName,
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: config.AMIName,
|
||||
BlockDevices: config.BlockDevices.BuildAMIDevices(),
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
|
||||
@@ -24,6 +24,7 @@ type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.BlockDevices `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
AccountId string `mapstructure:"account_id"`
|
||||
@@ -32,12 +33,11 @@ type Config struct {
|
||||
BundleUploadCommand string `mapstructure:"bundle_upload_command"`
|
||||
BundleVolCommand string `mapstructure:"bundle_vol_command"`
|
||||
S3Bucket string `mapstructure:"s3_bucket"`
|
||||
Tags map[string]string
|
||||
X509CertPath string `mapstructure:"x509_cert_path"`
|
||||
X509KeyPath string `mapstructure:"x509_key_path"`
|
||||
X509UploadPath string `mapstructure:"x509_upload_path"`
|
||||
|
||||
tpl *common.Template
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -51,7 +51,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -193,10 +193,12 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&awscommon.StepRunSourceInstance{
|
||||
ExpectedRootDevice: "instance-store",
|
||||
InstanceType: b.config.InstanceType,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
SubnetId: b.config.SubnetId,
|
||||
BlockDevices: b.config.BlockDevices,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
|
||||
@@ -208,13 +210,19 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&StepBundleVolume{},
|
||||
&StepUploadBundle{},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepCreateTags{Tags: b.config.Tags},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
ProductCodes: b.config.AMIProductCodes,
|
||||
},
|
||||
&awscommon.StepAMIRegionCopy{
|
||||
Regions: b.config.AMIRegions,
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
&awscommon.StepCreateTags{
|
||||
Tags: b.config.AMITags,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -27,25 +27,6 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
|
||||
x509RemoteCertPath := state["x509RemoteCertPath"].(string)
|
||||
x509RemoteKeyPath := state["x509RemoteKeyPath"].(string)
|
||||
|
||||
// Verify the AMI tools are available
|
||||
ui.Say("Checking for EC2 AMI tools...")
|
||||
cmd := &packer.RemoteCmd{Command: "ec2-ami-tools-version"}
|
||||
if err := comm.Start(cmd); err != nil {
|
||||
state["error"] = fmt.Errorf("Error checking for AMI tools: %s", err)
|
||||
ui.Error(state["error"].(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
cmd.Wait()
|
||||
|
||||
if cmd.ExitStatus != 0 {
|
||||
state["error"] = fmt.Errorf(
|
||||
"The EC2 AMI tools could not be detected. These must be manually\n" +
|
||||
"via a provisioner or some other means and are required for Packer\n" +
|
||||
"to create an instance-store AMI.")
|
||||
ui.Error(state["error"].(error).Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Bundle the volume
|
||||
var err error
|
||||
config.BundleVolCommand, err = config.tpl.Process(config.BundleVolCommand, bundleCmdData{
|
||||
@@ -65,7 +46,7 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
|
||||
}
|
||||
|
||||
ui.Say("Bundling the volume...")
|
||||
cmd = new(packer.RemoteCmd)
|
||||
cmd := new(packer.RemoteCmd)
|
||||
cmd.Command = config.BundleVolCommand
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
|
||||
|
||||
@@ -20,6 +20,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
ImageLocation: manifestPath,
|
||||
Name: config.AMIName,
|
||||
BlockDevices: config.BlockDevices.BuildAMIDevices(),
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
|
||||
+39
-18
@@ -43,7 +43,11 @@ type DigitalOceanClient struct {
|
||||
// Creates a new client for communicating with DO
|
||||
func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
|
||||
c := &DigitalOceanClient{
|
||||
client: http.DefaultClient,
|
||||
client: &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
},
|
||||
BaseURL: DIGITALOCEAN_API_URL,
|
||||
ClientID: client,
|
||||
APIKey: key,
|
||||
@@ -53,10 +57,9 @@ func (d DigitalOceanClient) New(client string, key string) *DigitalOceanClient {
|
||||
|
||||
// Creates an SSH Key and returns it's id
|
||||
func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
||||
// Escape the public key
|
||||
pub = url.QueryEscape(pub)
|
||||
|
||||
params := fmt.Sprintf("name=%v&ssh_pub_key=%v", name, pub)
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
params.Set("ssh_pub_key", pub)
|
||||
|
||||
body, err := NewRequest(d, "ssh_keys/new", params)
|
||||
if err != nil {
|
||||
@@ -72,15 +75,18 @@ func (d DigitalOceanClient) CreateKey(name string, pub string) (uint, error) {
|
||||
// Destroys an SSH key
|
||||
func (d DigitalOceanClient) DestroyKey(id uint) error {
|
||||
path := fmt.Sprintf("ssh_keys/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
return err
|
||||
}
|
||||
|
||||
// Creates a droplet and returns it's id
|
||||
func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, region uint, keyId uint) (uint, error) {
|
||||
params := fmt.Sprintf(
|
||||
"name=%v&image_id=%v&size_id=%v®ion_id=%v&ssh_key_ids=%v",
|
||||
name, image, size, region, keyId)
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
params.Set("size_id", fmt.Sprintf("%v", size))
|
||||
params.Set("image_id", fmt.Sprintf("%v", image))
|
||||
params.Set("region_id", fmt.Sprintf("%v", region))
|
||||
params.Set("ssh_key_ids", fmt.Sprintf("%v", keyId))
|
||||
|
||||
body, err := NewRequest(d, "droplets/new", params)
|
||||
if err != nil {
|
||||
@@ -96,7 +102,7 @@ func (d DigitalOceanClient) CreateDroplet(name string, size uint, image uint, re
|
||||
// Destroys a droplet
|
||||
func (d DigitalOceanClient) DestroyDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -104,7 +110,16 @@ func (d DigitalOceanClient) DestroyDroplet(id uint) error {
|
||||
func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/power_off", id)
|
||||
|
||||
_, err := NewRequest(d, path, "")
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// Shutsdown a droplet. This is a "soft" shutdown.
|
||||
func (d DigitalOceanClient) ShutdownDroplet(id uint) error {
|
||||
path := fmt.Sprintf("droplets/%v/shutdown", id)
|
||||
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -112,7 +127,9 @@ func (d DigitalOceanClient) PowerOffDroplet(id uint) error {
|
||||
// Creates a snaphot of a droplet by it's ID
|
||||
func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
|
||||
path := fmt.Sprintf("droplets/%v/snapshot", id)
|
||||
params := fmt.Sprintf("name=%v", name)
|
||||
|
||||
params := url.Values{}
|
||||
params.Set("name", name)
|
||||
|
||||
_, err := NewRequest(d, path, params)
|
||||
|
||||
@@ -121,7 +138,7 @@ func (d DigitalOceanClient) CreateSnapshot(id uint, name string) error {
|
||||
|
||||
// Returns all available images.
|
||||
func (d DigitalOceanClient) Images() ([]Image, error) {
|
||||
resp, err := NewRequest(d, "images", "")
|
||||
resp, err := NewRequest(d, "images", url.Values{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -137,7 +154,7 @@ func (d DigitalOceanClient) Images() ([]Image, error) {
|
||||
// Destroys an image by its ID.
|
||||
func (d DigitalOceanClient) DestroyImage(id uint) error {
|
||||
path := fmt.Sprintf("images/%d/destroy", id)
|
||||
_, err := NewRequest(d, path, "")
|
||||
_, err := NewRequest(d, path, url.Values{})
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -145,7 +162,7 @@ func (d DigitalOceanClient) DestroyImage(id uint) error {
|
||||
func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
|
||||
path := fmt.Sprintf("droplets/%v", id)
|
||||
|
||||
body, err := NewRequest(d, path, "")
|
||||
body, err := NewRequest(d, path, url.Values{})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
@@ -165,10 +182,14 @@ func (d DigitalOceanClient) DropletStatus(id uint) (string, string, error) {
|
||||
|
||||
// Sends an api request and returns a generic map[string]interface of
|
||||
// the response.
|
||||
func NewRequest(d DigitalOceanClient, path string, params string) (map[string]interface{}, error) {
|
||||
func NewRequest(d DigitalOceanClient, path string, params url.Values) (map[string]interface{}, error) {
|
||||
client := d.client
|
||||
url := fmt.Sprintf("%s/%s?%s&client_id=%s&api_key=%s",
|
||||
DIGITALOCEAN_API_URL, path, params, d.ClientID, d.APIKey)
|
||||
|
||||
// Add the authentication parameters
|
||||
params.Set("client_id", d.ClientID)
|
||||
params.Set("api_key", d.APIKey)
|
||||
|
||||
url := fmt.Sprintf("%s/%s?%s", DIGITALOCEAN_API_URL, path, params.Encode())
|
||||
|
||||
var decodedResponse map[string]interface{}
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ type config struct {
|
||||
eventDelay time.Duration
|
||||
stateTimeout time.Duration
|
||||
|
||||
tpl *common.Template
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -57,7 +57,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -206,6 +206,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
SSHWaitTimeout: 5 * time.Minute,
|
||||
},
|
||||
new(common.StepProvision),
|
||||
new(stepShutdown),
|
||||
new(stepPowerOff),
|
||||
new(stepSnapshot),
|
||||
}
|
||||
|
||||
@@ -32,15 +32,14 @@ func (s *stepPowerOff) Run(state map[string]interface{}) multistep.StepAction {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Waiting for droplet to power off...")
|
||||
log.Println("Waiting for poweroff event to complete...")
|
||||
|
||||
err = waitForDropletState("off", dropletId, client, c)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
// This arbitrary sleep is because we can't wait for the state
|
||||
// of the droplet to be 'off', as stepShutdown should already
|
||||
// have accomplished that, and the state indicator is the same.
|
||||
// We just have to assume that this event will process quickly.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.eventDelay)
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepShutdown struct{}
|
||||
|
||||
func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
client := state["client"].(*DigitalOceanClient)
|
||||
c := state["config"].(config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
dropletId := state["droplet_id"].(uint)
|
||||
|
||||
// Sleep arbitrarily before sending the request
|
||||
// Otherwise we get "pending event" errors, even though there isn't
|
||||
// one.
|
||||
log.Printf("Sleeping for %v, event_delay", c.RawEventDelay)
|
||||
time.Sleep(c.eventDelay)
|
||||
|
||||
err := client.ShutdownDroplet(dropletId)
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error shutting down droplet: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Waiting for droplet to shutdown...")
|
||||
|
||||
err = waitForDropletState("off", dropletId, client, c)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for droplet to become 'off': %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepShutdown) Cleanup(state map[string]interface{}) {
|
||||
// no cleanup
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"os"
|
||||
)
|
||||
|
||||
// AccessConfig is for common configuration related to openstack access
|
||||
type AccessConfig struct {
|
||||
Username string `mapstructure:"username"`
|
||||
Password string `mapstructure:"password"`
|
||||
Provider string `mapstructure:"provider"`
|
||||
}
|
||||
|
||||
// Auth returns a valid Auth object for access to openstack services, or
|
||||
// an error if the authentication couldn't be resolved.
|
||||
func (c *AccessConfig) Auth() (gophercloud.AccessProvider, error) {
|
||||
username := c.Username
|
||||
password := c.Password
|
||||
provider := c.Provider
|
||||
|
||||
if username == "" {
|
||||
username = os.Getenv("SDK_USERNAME")
|
||||
}
|
||||
if password == "" {
|
||||
password = os.Getenv("SDK_PASSWORD")
|
||||
}
|
||||
if provider == "" {
|
||||
provider = os.Getenv("SDK_PROVIDER")
|
||||
}
|
||||
|
||||
authoptions := gophercloud.AuthOptions{
|
||||
Username: username,
|
||||
Password: password,
|
||||
AllowReauth: true,
|
||||
}
|
||||
|
||||
return gophercloud.Authenticate(provider, authoptions)
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"username": &c.Username,
|
||||
"password": &c.Password,
|
||||
"provider": &c.Provider,
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testAccessConfig() *AccessConfig {
|
||||
return &AccessConfig{}
|
||||
}
|
||||
|
||||
func TestAccessConfigPrepare_Region(t *testing.T) {
|
||||
c := testAccessConfig()
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
)
|
||||
|
||||
// Artifact is an artifact implementation that contains built images.
|
||||
type Artifact struct {
|
||||
// ImageId of built image
|
||||
ImageId string
|
||||
|
||||
// BuilderId is the unique ID for the builder that created this image
|
||||
BuilderIdValue string
|
||||
|
||||
// OpenStack connection for performing API stuff.
|
||||
Conn gophercloud.CloudServersProvider
|
||||
}
|
||||
|
||||
func (a *Artifact) BuilderId() string {
|
||||
return a.BuilderIdValue
|
||||
}
|
||||
|
||||
func (*Artifact) Files() []string {
|
||||
// We have no files
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *Artifact) Id() string {
|
||||
return a.ImageId
|
||||
}
|
||||
|
||||
func (a *Artifact) String() string {
|
||||
return fmt.Sprintf("An image was created: %v", a.ImageId)
|
||||
}
|
||||
|
||||
func (a *Artifact) Destroy() error {
|
||||
log.Printf("Destroying image: %d", a.ImageId)
|
||||
return a.Conn.DeleteImageById(a.ImageId)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestArtifact_Impl(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
var actual packer.Artifact
|
||||
assert.Implementor(&Artifact{}, &actual, "should be an Artifact")
|
||||
}
|
||||
|
||||
func TestArtifactId(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
expected := `b8cdf55b-c916-40bd-b190-389ec144c4ed`
|
||||
|
||||
a := &Artifact{
|
||||
ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed",
|
||||
}
|
||||
|
||||
result := a.Id()
|
||||
assert.Equal(result, expected, "should match output")
|
||||
}
|
||||
|
||||
func TestArtifactString(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
expected := "An image was created: b8cdf55b-c916-40bd-b190-389ec144c4ed"
|
||||
|
||||
a := &Artifact{
|
||||
ImageId: "b8cdf55b-c916-40bd-b190-389ec144c4ed",
|
||||
}
|
||||
result := a.String()
|
||||
assert.Equal(result, expected, "should match output")
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
// The openstack package contains a packer.Builder implementation that
|
||||
// builds Images for openstack.
|
||||
|
||||
package openstack
|
||||
|
||||
import (
|
||||
//"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
const BuilderId = "mitchellh.openstack"
|
||||
|
||||
type config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
AccessConfig `mapstructure:",squash"`
|
||||
ImageConfig `mapstructure:",squash"`
|
||||
RunConfig `mapstructure:",squash"`
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
md, err := common.DecodeConfig(&b.config, raws...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.ImageConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
log.Printf("Config: %+v", b.config)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
auth, err := b.config.AccessConfig.Auth()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
api := &gophercloud.ApiCriteria{
|
||||
Name: "cloudServersOpenStack",
|
||||
Region: "DFW",
|
||||
VersionId: "2",
|
||||
UrlChoice: gophercloud.PublicURL,
|
||||
}
|
||||
csp, err := gophercloud.ServersApi(auth, *api)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Setup the state bag and initial state for the steps
|
||||
state := make(map[string]interface{})
|
||||
state["config"] = b.config
|
||||
state["csp"] = csp
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
// Build the steps
|
||||
steps := []multistep.Step{
|
||||
&StepKeyPair{},
|
||||
&StepRunSourceServer{
|
||||
Name: b.config.ImageName,
|
||||
Flavor: b.config.Flavor,
|
||||
SourceImage: b.config.SourceImage,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: SSHAddress(csp, b.config.SSHPort),
|
||||
SSHConfig: SSHConfig(b.config.SSHUsername),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&stepCreateImage{},
|
||||
}
|
||||
|
||||
// Run!
|
||||
if b.config.PackerDebug {
|
||||
b.runner = &multistep.DebugRunner{
|
||||
Steps: steps,
|
||||
PauseFn: common.MultistepDebugFn(ui),
|
||||
}
|
||||
} else {
|
||||
b.runner = &multistep.BasicRunner{Steps: steps}
|
||||
}
|
||||
|
||||
b.runner.Run(state)
|
||||
|
||||
// If there was an error, return that
|
||||
if rawErr, ok := state["error"]; ok {
|
||||
return nil, rawErr.(error)
|
||||
}
|
||||
|
||||
// Build the artifact and return it
|
||||
artifact := &Artifact{
|
||||
ImageId: state["image"].(string),
|
||||
BuilderIdValue: BuilderId,
|
||||
Conn: csp,
|
||||
}
|
||||
|
||||
return artifact, nil
|
||||
}
|
||||
|
||||
func (b *Builder) Cancel() {
|
||||
if b.runner != nil {
|
||||
log.Println("Cancelling the step runner...")
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"username": "foo",
|
||||
"password": "bar",
|
||||
"provider": "foo",
|
||||
"image_name": "foo",
|
||||
"source_image": "foo",
|
||||
"flavor": "foo",
|
||||
"ssh_username": "root",
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Builder{}
|
||||
if _, ok := raw.(packer.Builder); !ok {
|
||||
t.Fatalf("Builder should be a builder")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilder_Prepare_BadType(t *testing.T) {
|
||||
b := &Builder{}
|
||||
c := map[string]interface{}{
|
||||
"password": []string{},
|
||||
}
|
||||
|
||||
err := b.Prepare(c)
|
||||
if err == nil {
|
||||
t.Fatalf("prepare should fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_ImageName(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test good
|
||||
config["image_name"] = "foo"
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Test bad
|
||||
config["image_name"] = "foo {{"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad
|
||||
delete(config, "image_name")
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Add a random key
|
||||
config["i_should_not_be_valid"] = true
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
// ImageConfig is for common configuration related to creating Images.
|
||||
type ImageConfig struct {
|
||||
ImageName string `mapstructure:"image_name"`
|
||||
}
|
||||
|
||||
func (c *ImageConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"image_name": &c.ImageName,
|
||||
}
|
||||
|
||||
errs := make([]error, 0)
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if c.ImageName == "" {
|
||||
errs = append(errs, fmt.Errorf("An image_name must be specified"))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testImageConfig() *ImageConfig {
|
||||
return &ImageConfig{
|
||||
ImageName: "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageConfigPrepare_Region(t *testing.T) {
|
||||
c := testImageConfig()
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.ImageName = ""
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RunConfig contains configuration for running an instance from a source
|
||||
// image and details on how to access that launched image.
|
||||
type RunConfig struct {
|
||||
SourceImage string `mapstructure:"source_image"`
|
||||
Flavor string `mapstructure:"flavor"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
|
||||
// Unexported fields that are calculated from others
|
||||
sshTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare(t *packer.ConfigTemplate) []error {
|
||||
if t == nil {
|
||||
var err error
|
||||
t, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return []error{err}
|
||||
}
|
||||
}
|
||||
|
||||
// Defaults
|
||||
if c.SSHUsername == "" {
|
||||
c.SSHUsername = "root"
|
||||
}
|
||||
|
||||
if c.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
}
|
||||
|
||||
if c.RawSSHTimeout == "" {
|
||||
c.RawSSHTimeout = "1m"
|
||||
}
|
||||
|
||||
// Validation
|
||||
var err error
|
||||
errs := make([]error, 0)
|
||||
if c.SourceImage == "" {
|
||||
errs = append(errs, errors.New("A source_image must be specified"))
|
||||
}
|
||||
|
||||
if c.Flavor == "" {
|
||||
errs = append(errs, errors.New("A flavor must be specified"))
|
||||
}
|
||||
|
||||
if c.SSHUsername == "" {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"flavlor": &c.Flavor,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"source_image": &c.SourceImage,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = t.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
c.sshTimeout, err = time.ParseDuration(c.RawSSHTimeout)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Failed parsing ssh_timeout: %s", err))
|
||||
}
|
||||
|
||||
return errs
|
||||
}
|
||||
|
||||
func (c *RunConfig) SSHTimeout() time.Duration {
|
||||
return c.sshTimeout
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Clear out the openstack env vars so they don't
|
||||
// affect our tests.
|
||||
os.Setenv("SDK_USERNAME", "")
|
||||
os.Setenv("SDK_PASSWORD", "")
|
||||
os.Setenv("SDK_PROVIDER", "")
|
||||
}
|
||||
|
||||
func testRunConfig() *RunConfig {
|
||||
return &RunConfig{
|
||||
SourceImage: "abcd",
|
||||
Flavor: "m1.small",
|
||||
SSHUsername: "root",
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
err := c.Prepare(nil)
|
||||
if len(err) > 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_InstanceType(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.Flavor = ""
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SourceImage(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.SourceImage = ""
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.SSHPort = 0
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.SSHPort != 22 {
|
||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
||||
}
|
||||
|
||||
c.SSHPort = 44
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.SSHPort != 44 {
|
||||
t.Fatalf("invalid value: %d", c.SSHPort)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.RawSSHTimeout = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.RawSSHTimeout = "bad"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
|
||||
c := testRunConfig()
|
||||
c.SSHUsername = ""
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StateRefreshFunc is a function type used for StateChangeConf that is
|
||||
// responsible for refreshing the item being watched for a state change.
|
||||
//
|
||||
// It returns three results. `result` is any object that will be returned
|
||||
// as the final object after waiting for state change. This allows you to
|
||||
// return the final updated object, for example an openstack instance after
|
||||
// refreshing it.
|
||||
//
|
||||
// `state` is the latest state of that object. And `err` is any error that
|
||||
// may have happened while refreshing the state.
|
||||
type StateRefreshFunc func() (result interface{}, state string, progress int, err error)
|
||||
|
||||
// StateChangeConf is the configuration struct used for `WaitForState`.
|
||||
type StateChangeConf struct {
|
||||
Pending []string
|
||||
Refresh StateRefreshFunc
|
||||
StepState map[string]interface{}
|
||||
Target string
|
||||
}
|
||||
|
||||
// ServerStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an openstacn server.
|
||||
func ServerStateRefreshFunc(csp gophercloud.CloudServersProvider, s *gophercloud.Server) StateRefreshFunc {
|
||||
return func() (interface{}, string, int, error) {
|
||||
resp, err := csp.ServerById(s.Id)
|
||||
if err != nil {
|
||||
log.Printf("Error on ServerStateRefresh: %s", err)
|
||||
return nil, "", 0, err
|
||||
}
|
||||
|
||||
return resp, resp.Status, resp.Progress, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
for {
|
||||
var currentProgress int
|
||||
var currentState string
|
||||
i, currentState, currentProgress, err = conf.Refresh()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if currentState == conf.Target {
|
||||
return
|
||||
}
|
||||
|
||||
if conf.StepState != nil {
|
||||
if _, ok := conf.StepState[multistep.StateCancelled]; ok {
|
||||
return nil, errors.New("interrupted")
|
||||
}
|
||||
}
|
||||
|
||||
found := false
|
||||
for _, allowed := range conf.Pending {
|
||||
if currentState == allowed {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
fmt.Errorf("unexpected state '%s', wanted target '%s'", currentState, conf.Target)
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("Waiting for state to become: %s currently %s (%d%%)", conf.Target, currentState, currentProgress)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the server AccessIPv4 setting..
|
||||
func SSHAddress(csp gophercloud.CloudServersProvider, port int) func(map[string]interface{}) (string, error) {
|
||||
return func(state map[string]interface{}) (string, error) {
|
||||
for j := 0; j < 2; j++ {
|
||||
s := state["server"].(*gophercloud.Server)
|
||||
if s.AccessIPv4 != "" {
|
||||
return fmt.Sprintf("%s:%d", s.AccessIPv4, port), nil
|
||||
}
|
||||
serverState, err := csp.ServerById(s.Id)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
state["server"] = serverState
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
return "", errors.New("couldn't determine IP address for server")
|
||||
}
|
||||
}
|
||||
|
||||
// SSHConfig returns a function that can be used for the SSH communicator
|
||||
// config for connecting to the instance created over SSH using the generated
|
||||
// private key.
|
||||
func SSHConfig(username string) func(map[string]interface{}) (*gossh.ClientConfig, error) {
|
||||
return func(state map[string]interface{}) (*gossh.ClientConfig, error) {
|
||||
privateKey := state["privateKey"].(string)
|
||||
|
||||
keyring := new(ssh.SimpleKeychain)
|
||||
if err := keyring.AddPEMKey(privateKey); err != nil {
|
||||
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
}
|
||||
|
||||
return &gossh.ClientConfig{
|
||||
User: username,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepCreateImage struct{}
|
||||
|
||||
func (s *stepCreateImage) Run(state map[string]interface{}) multistep.StepAction {
|
||||
csp := state["csp"].(gophercloud.CloudServersProvider)
|
||||
config := state["config"].(config)
|
||||
server := state["server"].(*gophercloud.Server)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the image: %s", config.ImageName))
|
||||
createOpts := gophercloud.CreateImage{
|
||||
Name: config.ImageName,
|
||||
}
|
||||
imageId, err := csp.CreateImage(server.Id, createOpts)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error creating image: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the Image ID in the state
|
||||
ui.Say(fmt.Sprintf("Image: %s", imageId))
|
||||
state["image"] = imageId
|
||||
|
||||
// Wait for the image to become ready
|
||||
ui.Say("Waiting for image to become ready...")
|
||||
if err := WaitForImage(csp, imageId); err != nil {
|
||||
err := fmt.Errorf("Error waiting for image: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepCreateImage) Cleanup(map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
|
||||
// WaitForImage waits for the given Image ID to become ready.
|
||||
func WaitForImage(csp gophercloud.CloudServersProvider, imageId string) error {
|
||||
for {
|
||||
image, err := csp.ImageById(imageId)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if image.Status == "ACTIVE" {
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Printf("Waiting for image creation status: %s (%d%%)", image.Status, image.Progress)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"cgl.tideland.biz/identifier"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
)
|
||||
|
||||
type StepKeyPair struct {
|
||||
keyName string
|
||||
}
|
||||
|
||||
func (s *StepKeyPair) Run(state map[string]interface{}) multistep.StepAction {
|
||||
csp := state["csp"].(gophercloud.CloudServersProvider)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Creating temporary keypair for this instance...")
|
||||
keyName := fmt.Sprintf("packer %s", hex.EncodeToString(identifier.NewUUID().Raw()))
|
||||
log.Printf("temporary keypair name: %s", keyName)
|
||||
keyResp, err := csp.CreateKeyPair(gophercloud.NewKeyPair{Name: keyName})
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error creating temporary keypair: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the keyname so we know to delete it later
|
||||
s.keyName = keyName
|
||||
|
||||
// Set some state data for use in future steps
|
||||
state["keyPair"] = keyName
|
||||
state["privateKey"] = keyResp.PrivateKey
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepKeyPair) Cleanup(state map[string]interface{}) {
|
||||
// If no key name is set, then we never created it, so just return
|
||||
if s.keyName == "" {
|
||||
return
|
||||
}
|
||||
|
||||
csp := state["csp"].(gophercloud.CloudServersProvider)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary keypair...")
|
||||
err := csp.DeleteKeyPair(s.keyName)
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up keypair. Please delete the key manually: %s", s.keyName))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package openstack
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/rackspace/gophercloud"
|
||||
"log"
|
||||
)
|
||||
|
||||
type StepRunSourceServer struct {
|
||||
Flavor string
|
||||
Name string
|
||||
SourceImage string
|
||||
|
||||
server *gophercloud.Server
|
||||
}
|
||||
|
||||
func (s *StepRunSourceServer) Run(state map[string]interface{}) multistep.StepAction {
|
||||
csp := state["csp"].(gophercloud.CloudServersProvider)
|
||||
keyName := state["keyPair"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// XXX - validate image and flavor is available
|
||||
|
||||
server := gophercloud.NewServer{
|
||||
Name: s.Name,
|
||||
ImageRef: s.SourceImage,
|
||||
FlavorRef: s.Flavor,
|
||||
KeyPairName: keyName,
|
||||
}
|
||||
|
||||
serverResp, err := csp.CreateServer(server)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error launching source server: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.server, err = csp.ServerById(serverResp.Id)
|
||||
log.Printf("server id: %s", s.server.Id)
|
||||
|
||||
ui.Say(fmt.Sprintf("Waiting for server (%s) to become ready...", s.server.Id))
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"BUILD"},
|
||||
Target: "ACTIVE",
|
||||
Refresh: ServerStateRefreshFunc(csp, s.server),
|
||||
StepState: state,
|
||||
}
|
||||
latestServer, err := WaitForState(&stateChange)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error waiting for server (%s) to become ready: %s", s.server.Id, err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
s.server = latestServer.(*gophercloud.Server)
|
||||
state["server"] = s.server
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepRunSourceServer) Cleanup(state map[string]interface{}) {
|
||||
if s.server == nil {
|
||||
return
|
||||
}
|
||||
|
||||
csp := state["csp"].(gophercloud.CloudServersProvider)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Terminating the source server...")
|
||||
if err := csp.DeleteServerById(s.server.Id); err != nil {
|
||||
ui.Error(fmt.Sprintf("Error terminating server, may still be around: %s", err))
|
||||
return
|
||||
}
|
||||
|
||||
stateChange := StateChangeConf{
|
||||
Pending: []string{"ACTIVE", "BUILD", "REBUILD", "SUSPENDED"},
|
||||
Refresh: ServerStateRefreshFunc(csp, s.server),
|
||||
Target: "DELETED",
|
||||
}
|
||||
|
||||
WaitForState(&stateChange)
|
||||
}
|
||||
@@ -18,7 +18,6 @@ const BuilderId = "mitchellh.virtualbox"
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
driver Driver
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
@@ -28,6 +27,7 @@ type config struct {
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
Format string `mapstructure:"format"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
@@ -38,11 +38,12 @@ type config struct {
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
ISOChecksum string `mapstructure:"iso_checksum"`
|
||||
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
@@ -51,13 +52,14 @@ type config struct {
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
|
||||
RawBootWait string `mapstructure:"boot_wait"`
|
||||
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
||||
|
||||
bootWait time.Duration ``
|
||||
shutdownTimeout time.Duration ``
|
||||
sshWaitTimeout time.Duration ``
|
||||
tpl *common.Template
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
@@ -66,7 +68,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -131,22 +133,25 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
if b.config.Format == "" {
|
||||
b.config.Format = "ovf"
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"guest_additions_path": &b.config.GuestAdditionsPath,
|
||||
"guest_additions_url": &b.config.GuestAdditionsURL,
|
||||
"guest_additions_sha256": &b.config.GuestAdditionsSHA256,
|
||||
"guest_os_type": &b.config.GuestOSType,
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.ISOUrl,
|
||||
"iso_url": &b.config.RawSingleISOUrl,
|
||||
"output_directory": &b.config.OutputDir,
|
||||
"shutdown_command": &b.config.ShutdownCommand,
|
||||
"ssh_password": &b.config.SSHPassword,
|
||||
"ssh_username": &b.config.SSHUser,
|
||||
"virtualbox_version_file": &b.config.VBoxVersionFile,
|
||||
"vm_name": &b.config.VMName,
|
||||
"format": &b.config.Format,
|
||||
"boot_wait": &b.config.RawBootWait,
|
||||
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||
@@ -161,6 +166,27 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
for i, url := range b.config.ISOUrls {
|
||||
var err error
|
||||
b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
validates := map[string]*string{
|
||||
"guest_additions_path": &b.config.GuestAdditionsPath,
|
||||
"guest_additions_url": &b.config.GuestAdditionsURL,
|
||||
}
|
||||
|
||||
for n, ptr := range validates {
|
||||
if err := b.config.tpl.Validate(*ptr); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error parsing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, command := range b.config.BootCommand {
|
||||
if err := b.config.tpl.Validate(command); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
@@ -178,6 +204,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if !(b.config.Format == "ovf" || b.config.Format == "ova") {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("invalid format, only 'ovf' or 'ova' are allowed"))
|
||||
}
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
@@ -202,14 +233,21 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.ISOUrl == "" {
|
||||
if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("An iso_url must be specified."))
|
||||
} else {
|
||||
b.config.ISOUrl, err = common.DownloadableURL(b.config.ISOUrl)
|
||||
errs, errors.New("One of iso_url or iso_urls must be specified."))
|
||||
} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
|
||||
} else if b.config.RawSingleISOUrl != "" {
|
||||
b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
|
||||
}
|
||||
|
||||
for i, url := range b.config.ISOUrls {
|
||||
b.config.ISOUrls[i], err = common.DownloadableURL(url)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("iso_url: %s", err))
|
||||
errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,14 +255,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.GuestAdditionsSHA256 = strings.ToLower(b.config.GuestAdditionsSHA256)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsURL != "" {
|
||||
b.config.GuestAdditionsURL, err = common.DownloadableURL(b.config.GuestAdditionsURL)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("guest_additions_url: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if !b.config.PackerForce {
|
||||
if _, err := os.Stat(b.config.OutputDir); err == nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
@@ -253,6 +283,16 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs, fmt.Errorf("Failed parsing shutdown_timeout: %s", err))
|
||||
}
|
||||
|
||||
if b.config.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SSHHostPortMin > b.config.SSHHostPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ssh_host_port_min must be less than ssh_host_port_max"))
|
||||
@@ -278,12 +318,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
b.driver, err = b.newDriver()
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -292,9 +326,21 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
// Create the driver that we'll use to communicate with VirtualBox
|
||||
driver, err := b.newDriver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err)
|
||||
}
|
||||
|
||||
steps := []multistep.Step{
|
||||
new(stepDownloadGuestAdditions),
|
||||
new(stepDownloadISO),
|
||||
&common.StepDownload{
|
||||
Checksum: b.config.ISOChecksum,
|
||||
ChecksumType: b.config.ISOChecksumType,
|
||||
Description: "ISO",
|
||||
ResultKey: "iso_path",
|
||||
Url: b.config.ISOUrls,
|
||||
},
|
||||
new(stepPrepareOutputDir),
|
||||
&common.StepCreateFloppy{
|
||||
Files: b.config.FloppyFiles,
|
||||
@@ -325,7 +371,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
state := make(map[string]interface{})
|
||||
state["cache"] = cache
|
||||
state["config"] = &b.config
|
||||
state["driver"] = b.driver
|
||||
state["driver"] = driver
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
|
||||
@@ -8,6 +8,36 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var testPem = `
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
|
||||
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
|
||||
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
|
||||
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
|
||||
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
|
||||
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
|
||||
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
|
||||
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
|
||||
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
|
||||
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
|
||||
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
|
||||
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
|
||||
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
|
||||
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
|
||||
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
|
||||
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
|
||||
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
|
||||
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
|
||||
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
|
||||
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
|
||||
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
|
||||
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
|
||||
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
|
||||
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
|
||||
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"iso_checksum": "foo",
|
||||
@@ -58,6 +88,10 @@ func TestBuilderPrepare_Defaults(t *testing.T) {
|
||||
if b.config.VMName != "packer-foo" {
|
||||
t.Errorf("bad vm name: %s", b.config.VMName)
|
||||
}
|
||||
|
||||
if b.config.Format != "ovf" {
|
||||
t.Errorf("bad format: %s", b.config.Format)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_BootWait(t *testing.T) {
|
||||
@@ -84,6 +118,7 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["boot_wait"] = "5s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -210,6 +245,7 @@ func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) {
|
||||
}
|
||||
|
||||
config["guest_additions_url"] = "http://www.packer.io"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
@@ -230,6 +266,7 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
|
||||
// Bad
|
||||
config["http_port_min"] = -500
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -238,6 +275,35 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
// Good
|
||||
config["http_port_min"] = 500
|
||||
config["http_port_max"] = 1000
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_Format(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Bad
|
||||
config["format"] = "illegal value"
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Good
|
||||
config["format"] = "ova"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Good
|
||||
config["format"] = "ovf"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -269,6 +335,7 @@ func TestBuilderPrepare_ISOChecksum(t *testing.T) {
|
||||
|
||||
// Test good
|
||||
config["iso_checksum"] = "FOo"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -292,6 +359,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
|
||||
|
||||
// Test good
|
||||
config["iso_checksum_type"] = "mD5"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -303,6 +371,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
|
||||
|
||||
// Test unknown
|
||||
config["iso_checksum_type"] = "fake"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -312,18 +381,59 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
|
||||
func TestBuilderPrepare_ISOUrl(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
delete(config, "iso_url")
|
||||
delete(config, "iso_urls")
|
||||
|
||||
// Test both epty
|
||||
config["iso_url"] = ""
|
||||
b = Builder{}
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test iso_url set
|
||||
config["iso_url"] = "http://www.packer.io"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"http://www.packer.io"}
|
||||
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||
}
|
||||
|
||||
// Test both set
|
||||
config["iso_url"] = "http://www.packer.io"
|
||||
config["iso_urls"] = []string{"http://www.packer.io"}
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test just iso_urls set
|
||||
delete(config, "iso_url")
|
||||
config["iso_urls"] = []string{
|
||||
"http://www.packer.io",
|
||||
"http://www.hashicorp.com",
|
||||
}
|
||||
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected = []string{
|
||||
"http://www.packer.io",
|
||||
"http://www.hashicorp.com",
|
||||
}
|
||||
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
@@ -338,6 +448,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
config["output_directory"] = dir
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -345,6 +456,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["output_directory"] = "i-hope-i-dont-exist"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -364,6 +476,7 @@ func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["shutdown_timeout"] = "5s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -377,6 +490,7 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
|
||||
// Bad
|
||||
config["ssh_host_port_min"] = 1000
|
||||
config["ssh_host_port_max"] = 500
|
||||
b = Builder{}
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -384,6 +498,7 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
|
||||
|
||||
// Bad
|
||||
config["ssh_host_port_min"] = -500
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -392,23 +507,75 @@ func TestBuilderPrepare_SSHHostPort(t *testing.T) {
|
||||
// Good
|
||||
config["ssh_host_port_min"] = 500
|
||||
config["ssh_host_port_max"] = 1000
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_sshKeyPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
config["ssh_key_path"] = ""
|
||||
b = Builder{}
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
config["ssh_key_path"] = "/i/dont/exist"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad contents
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
defer tf.Close()
|
||||
|
||||
if _, err := tf.Write([]byte("HELLO!")); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
config["ssh_key_path"] = tf.Name()
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test good contents
|
||||
tf.Seek(0, 0)
|
||||
tf.Truncate(0)
|
||||
tf.Write([]byte(testPem))
|
||||
config["ssh_key_path"] = tf.Name()
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHUser(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
config["ssh_username"] = ""
|
||||
b = Builder{}
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
config["ssh_username"] = "exists"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -432,6 +599,7 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
|
||||
// Test with a bad value
|
||||
config["ssh_wait_timeout"] = "this is not good"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -439,6 +607,7 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["ssh_wait_timeout"] = "5s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
gossh "code.google.com/p/go.crypto/ssh"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
)
|
||||
|
||||
func sshAddress(state map[string]interface{}) (string, error) {
|
||||
@@ -14,12 +16,43 @@ func sshAddress(state map[string]interface{}) (string, error) {
|
||||
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
|
||||
config := state["config"].(*config)
|
||||
|
||||
auth := []gossh.ClientAuth{
|
||||
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||
gossh.ClientAuthKeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
keyring, err := sshKeyToKeyring(config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth = append(auth, gossh.ClientAuthKeyring(keyring))
|
||||
}
|
||||
|
||||
return &gossh.ClientConfig{
|
||||
User: config.SSHUser,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||
gossh.ClientAuthKeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
},
|
||||
Auth: auth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sshKeyToKeyring(path string) (gossh.ClientKeyring, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
keyBytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyring := new(ssh.SimpleKeychain)
|
||||
if err := keyring.AddPEMKey(string(keyBytes)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keyring, nil
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
@@ -13,7 +11,6 @@ import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
var additionsVersionMap = map[string]string{
|
||||
@@ -21,6 +18,10 @@ var additionsVersionMap = map[string]string{
|
||||
"4.1.23": "4.1.22",
|
||||
}
|
||||
|
||||
type guestAdditionsUrlTemplate struct {
|
||||
Version string
|
||||
}
|
||||
|
||||
// This step uploads a file containing the VirtualBox version, which
|
||||
// can be useful for various provisioning reasons.
|
||||
//
|
||||
@@ -30,7 +31,6 @@ type stepDownloadGuestAdditions struct{}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep.StepAction {
|
||||
var action multistep.StepAction
|
||||
cache := state["cache"].(packer.Cache)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
config := state["config"].(*config)
|
||||
@@ -61,87 +61,56 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep
|
||||
}
|
||||
}
|
||||
|
||||
checksumBytes, err := hex.DecodeString(checksum)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Couldn't decode checksum into bytes: %s", checksum)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Use the provided source (URL or file path) or generate it
|
||||
url := config.GuestAdditionsURL
|
||||
if url == "" {
|
||||
if url != "" {
|
||||
tplData := &guestAdditionsUrlTemplate{
|
||||
Version: version,
|
||||
}
|
||||
|
||||
url, err = config.tpl.Process(url, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing guest additions url: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
} else {
|
||||
url = fmt.Sprintf(
|
||||
"http://download.virtualbox.org/virtualbox/%s/%s",
|
||||
version,
|
||||
additionsName)
|
||||
}
|
||||
|
||||
log.Printf("Guest additions URL: %s", url)
|
||||
|
||||
log.Printf("Acquiring lock to download the guest additions ISO.")
|
||||
cachePath := cache.Lock(url)
|
||||
defer cache.Unlock(url)
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: url,
|
||||
TargetPath: cachePath,
|
||||
Hash: sha256.New(),
|
||||
Checksum: checksumBytes,
|
||||
url, err = common.DownloadableURL(url)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing guest additions url: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
ui.Say("Downloading VirtualBox guest additions. Progress will be shown periodically.")
|
||||
state["guest_additions_path"], action = s.progressDownload(download, state)
|
||||
return action
|
||||
log.Printf("Guest additions URL: %s", url)
|
||||
|
||||
downStep := &common.StepDownload{
|
||||
Checksum: checksum,
|
||||
ChecksumType: "sha256",
|
||||
Description: "Guest additions",
|
||||
ResultKey: "guest_additions_path",
|
||||
Url: []string{url},
|
||||
}
|
||||
|
||||
return downStep.Run(state)
|
||||
}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) Cleanup(state map[string]interface{}) {}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) progressDownload(c *common.DownloadClient, state map[string]interface{}) (string, multistep.StepAction) {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
var result string
|
||||
downloadCompleteCh := make(chan error, 1)
|
||||
|
||||
// Start a goroutine to actually do the download...
|
||||
go func() {
|
||||
var err error
|
||||
result, err = c.Get()
|
||||
downloadCompleteCh <- err
|
||||
}()
|
||||
|
||||
progressTicker := time.NewTicker(5 * time.Second)
|
||||
defer progressTicker.Stop()
|
||||
|
||||
// A loop that handles showing progress as well as timing out and handling
|
||||
// interrupts and all that.
|
||||
DownloadWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error downloading: %s", err)
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
|
||||
break DownloadWaitLoop
|
||||
case <-progressTicker.C:
|
||||
ui.Message(fmt.Sprintf("Download progress: %d%%", c.PercentProgress()))
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]interface{}, additionsVersion string, additionsName string) (string, multistep.StepAction) {
|
||||
// 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", additionsVersion)
|
||||
checksumsUrl := fmt.Sprintf(
|
||||
"http://download.virtualbox.org/virtualbox/%s/SHA256SUMS",
|
||||
additionsVersion)
|
||||
|
||||
checksumsFile, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
@@ -151,25 +120,23 @@ func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]in
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
defer os.Remove(checksumsFile.Name())
|
||||
|
||||
checksumsFile.Close()
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: checksumsUrl,
|
||||
TargetPath: checksumsFile.Name(),
|
||||
Hash: nil,
|
||||
downStep := &common.StepDownload{
|
||||
Description: "Guest additions checksums",
|
||||
ResultKey: "guest_additions_checksums_path",
|
||||
TargetPath: checksumsFile.Name(),
|
||||
Url: []string{checksumsUrl},
|
||||
}
|
||||
|
||||
log.Printf("Downloading guest addition checksums: %s", checksumsUrl)
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
checksumsPath, action := s.progressDownload(download, state)
|
||||
if action != multistep.ActionContinue {
|
||||
action := downStep.Run(state)
|
||||
if action == multistep.ActionHalt {
|
||||
return "", action
|
||||
}
|
||||
|
||||
// Next, we find the checksum for the file we're looking to download.
|
||||
// It is an error if the checksum cannot be found.
|
||||
checksumsF, err := os.Open(checksumsPath)
|
||||
checksumsF, err := os.Open(state["guest_additions_checksums_path"].(string))
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err)
|
||||
return "", multistep.ActionHalt
|
||||
@@ -199,7 +166,8 @@ func (s *stepDownloadGuestAdditions) downloadAdditionsSHA256(state map[string]in
|
||||
}
|
||||
|
||||
if checksum == "" {
|
||||
state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName)
|
||||
state["error"] = fmt.Errorf(
|
||||
"The checksum for the file '%s' could not be found.", additionsName)
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This step downloads the ISO specified.
|
||||
//
|
||||
// Uses:
|
||||
// cache packer.Cache
|
||||
// config *config
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
// iso_path string
|
||||
type stepDownloadISO struct{}
|
||||
|
||||
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
|
||||
cache := state["cache"].(packer.Cache)
|
||||
config := state["config"].(*config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
checksum, err := hex.DecodeString(config.ISOChecksum)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error parsing checksum: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Acquiring lock to download the ISO.")
|
||||
cachePath := cache.Lock(config.ISOUrl)
|
||||
defer cache.Unlock(config.ISOUrl)
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: config.ISOUrl,
|
||||
TargetPath: cachePath,
|
||||
CopyFile: false,
|
||||
Hash: common.HashForType(config.ISOChecksumType),
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
|
||||
downloadCompleteCh := make(chan error, 1)
|
||||
go func() {
|
||||
ui.Say("Copying or downloading ISO. Progress will be reported periodically.")
|
||||
cachePath, err = download.Get()
|
||||
downloadCompleteCh <- err
|
||||
}()
|
||||
|
||||
progressTicker := time.NewTicker(5 * time.Second)
|
||||
defer progressTicker.Stop()
|
||||
|
||||
DownloadWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error downloading ISO: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
break DownloadWaitLoop
|
||||
case <-progressTicker.C:
|
||||
ui.Message(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Path to ISO on disk: %s", cachePath)
|
||||
state["iso_path"] = cachePath
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
|
||||
@@ -50,7 +50,7 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
|
||||
}
|
||||
|
||||
// Export the VM to an OVF
|
||||
outputPath := filepath.Join(config.OutputDir, "packer.ovf")
|
||||
outputPath := filepath.Join(config.OutputDir, "packer."+config.Format)
|
||||
|
||||
command = []string{
|
||||
"export",
|
||||
|
||||
+85
-18
@@ -6,6 +6,7 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
@@ -19,7 +20,6 @@ const BuilderId = "mitchellh.vmware"
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
driver Driver
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
@@ -28,11 +28,12 @@ type config struct {
|
||||
|
||||
DiskName string `mapstructure:"vmdk_name"`
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
DiskTypeId string `mapstructure:"disk_type_id"`
|
||||
FloppyFiles []string `mapstructure:"floppy_files"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
ISOChecksum string `mapstructure:"iso_checksum"`
|
||||
ISOChecksumType string `mapstructure:"iso_checksum_type"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
ISOUrls []string `mapstructure:"iso_urls"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
Headless bool `mapstructure:"headless"`
|
||||
@@ -43,22 +44,26 @@ type config struct {
|
||||
SkipCompaction bool `mapstructure:"skip_compaction"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHKeyPath string `mapstructure:"ssh_key_path"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHSkipRequestPty bool `mapstructure:"ssh_skip_request_pty"`
|
||||
ToolsUploadFlavor string `mapstructure:"tools_upload_flavor"`
|
||||
ToolsUploadPath string `mapstructure:"tools_upload_path"`
|
||||
VMXData map[string]string `mapstructure:"vmx_data"`
|
||||
VMXTemplatePath string `mapstructure:"vmx_template_path"`
|
||||
VNCPortMin uint `mapstructure:"vnc_port_min"`
|
||||
VNCPortMax uint `mapstructure:"vnc_port_max"`
|
||||
|
||||
RawBootWait string `mapstructure:"boot_wait"`
|
||||
RawSingleISOUrl string `mapstructure:"iso_url"`
|
||||
RawShutdownTimeout string `mapstructure:"shutdown_timeout"`
|
||||
RawSSHWaitTimeout string `mapstructure:"ssh_wait_timeout"`
|
||||
|
||||
bootWait time.Duration ``
|
||||
shutdownTimeout time.Duration ``
|
||||
sshWaitTimeout time.Duration ``
|
||||
tpl *common.Template
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
@@ -67,7 +72,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = common.NewTemplate()
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -84,6 +89,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.DiskSize = 40000
|
||||
}
|
||||
|
||||
if b.config.DiskTypeId == "" {
|
||||
// Default is growable virtual disk split in 2GB files.
|
||||
b.config.DiskTypeId = "1"
|
||||
}
|
||||
|
||||
if b.config.FloppyFiles == nil {
|
||||
b.config.FloppyFiles = make([]string, 0)
|
||||
}
|
||||
@@ -135,7 +145,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
"http_directory": &b.config.HTTPDir,
|
||||
"iso_checksum": &b.config.ISOChecksum,
|
||||
"iso_checksum_type": &b.config.ISOChecksumType,
|
||||
"iso_url": &b.config.ISOUrl,
|
||||
"iso_url": &b.config.RawSingleISOUrl,
|
||||
"output_directory": &b.config.OutputDir,
|
||||
"shutdown_command": &b.config.ShutdownCommand,
|
||||
"ssh_password": &b.config.SSHPassword,
|
||||
@@ -145,6 +155,7 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
"boot_wait": &b.config.RawBootWait,
|
||||
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||
"vmx_template_path": &b.config.VMXTemplatePath,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
@@ -156,6 +167,15 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
for i, url := range b.config.ISOUrls {
|
||||
var err error
|
||||
b.config.ISOUrls[i], err = b.config.tpl.Process(url, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing iso_urls[%d]: %s", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, command := range b.config.BootCommand {
|
||||
if err := b.config.tpl.Validate(command); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
@@ -218,14 +238,21 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.ISOUrl == "" {
|
||||
if b.config.RawSingleISOUrl == "" && len(b.config.ISOUrls) == 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("An iso_url must be specified."))
|
||||
} else {
|
||||
b.config.ISOUrl, err = common.DownloadableURL(b.config.ISOUrl)
|
||||
errs, errors.New("One of iso_url or iso_urls must be specified."))
|
||||
} else if b.config.RawSingleISOUrl != "" && len(b.config.ISOUrls) > 0 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Only one of iso_url or iso_urls may be specified."))
|
||||
} else if b.config.RawSingleISOUrl != "" {
|
||||
b.config.ISOUrls = []string{b.config.RawSingleISOUrl}
|
||||
}
|
||||
|
||||
for i, url := range b.config.ISOUrls {
|
||||
b.config.ISOUrls[i], err = common.DownloadableURL(url)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("iso_url: %s", err))
|
||||
errs, fmt.Errorf("Failed to parse iso_url %d: %s", i+1, err))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,6 +264,16 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SSHKeyPath != "" {
|
||||
if _, err := os.Stat(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
} else if _, err := sshKeyToKeyring(b.config.SSHKeyPath); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("ssh_key_path is invalid: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SSHUser == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("An ssh_username must be specified."))
|
||||
@@ -275,17 +312,19 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs, fmt.Errorf("tools_upload_path invalid: %s", err))
|
||||
}
|
||||
|
||||
if b.config.VMXTemplatePath != "" {
|
||||
if err := b.validateVMXTemplatePath(); err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("vmx_template_path is invalid: %s", err))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if b.config.VNCPortMin > b.config.VNCPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("vnc_port_min must be less than vnc_port_max"))
|
||||
}
|
||||
|
||||
b.driver, err = NewDriver()
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed creating VMware driver: %s", err))
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -294,12 +333,24 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
|
||||
// Initialize the driver that will handle our interaction with VMware
|
||||
driver, err := NewDriver()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed creating VMware driver: %s", err)
|
||||
}
|
||||
|
||||
// Seed the random number generator
|
||||
rand.Seed(time.Now().UTC().UnixNano())
|
||||
|
||||
steps := []multistep.Step{
|
||||
&stepPrepareTools{},
|
||||
&stepDownloadISO{},
|
||||
&common.StepDownload{
|
||||
Checksum: b.config.ISOChecksum,
|
||||
ChecksumType: b.config.ISOChecksumType,
|
||||
Description: "ISO",
|
||||
ResultKey: "iso_path",
|
||||
Url: b.config.ISOUrls,
|
||||
},
|
||||
&stepPrepareOutputDir{},
|
||||
&common.StepCreateFloppy{
|
||||
Files: b.config.FloppyFiles,
|
||||
@@ -314,6 +365,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
SSHAddress: sshAddress,
|
||||
SSHConfig: sshConfig,
|
||||
SSHWaitTimeout: b.config.sshWaitTimeout,
|
||||
NoPty: b.config.SSHSkipRequestPty,
|
||||
},
|
||||
&stepUploadTools{},
|
||||
&common.StepProvision{},
|
||||
@@ -327,7 +379,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
state := make(map[string]interface{})
|
||||
state["cache"] = cache
|
||||
state["config"] = &b.config
|
||||
state["driver"] = b.driver
|
||||
state["driver"] = driver
|
||||
state["hook"] = hook
|
||||
state["ui"] = ui
|
||||
|
||||
@@ -384,3 +436,18 @@ func (b *Builder) Cancel() {
|
||||
b.runner.Cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Builder) validateVMXTemplatePath() error {
|
||||
f, err := os.Open(b.config.VMXTemplatePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
data, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return b.config.tpl.Validate(string(data))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,36 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
var testPem = `
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpQIBAAKCAQEAxd4iamvrwRJvtNDGQSIbNvvIQN8imXTRWlRY62EvKov60vqu
|
||||
hh+rDzFYAIIzlmrJopvOe0clqmi3mIP9dtkjPFrYflq52a2CF5q+BdwsJXuRHbJW
|
||||
LmStZUwW1khSz93DhvhmK50nIaczW63u4EO/jJb3xj+wxR1Nkk9bxi3DDsYFt8SN
|
||||
AzYx9kjlEYQ/+sI4/ATfmdV9h78SVotjScupd9KFzzi76gWq9gwyCBLRynTUWlyD
|
||||
2UOfJRkOvhN6/jKzvYfVVwjPSfA9IMuooHdScmC4F6KBKJl/zf/zETM0XyzIDNmH
|
||||
uOPbCiljq2WoRM+rY6ET84EO0kVXbfx8uxUsqQIDAQABAoIBAQCkPj9TF0IagbM3
|
||||
5BSs/CKbAWS4dH/D4bPlxx4IRCNirc8GUg+MRb04Xz0tLuajdQDqeWpr6iLZ0RKV
|
||||
BvreLF+TOdV7DNQ4XE4gSdJyCtCaTHeort/aordL3l0WgfI7mVk0L/yfN1PEG4YG
|
||||
E9q1TYcyrB3/8d5JwIkjabxERLglCcP+geOEJp+QijbvFIaZR/n2irlKW4gSy6ko
|
||||
9B0fgUnhkHysSg49ChHQBPQ+o5BbpuLrPDFMiTPTPhdfsvGGcyCGeqfBA56oHcSF
|
||||
K02Fg8OM+Bd1lb48LAN9nWWY4WbwV+9bkN3Ym8hO4c3a/Dxf2N7LtAQqWZzFjvM3
|
||||
/AaDvAgBAoGBAPLD+Xn1IYQPMB2XXCXfOuJewRY7RzoVWvMffJPDfm16O7wOiW5+
|
||||
2FmvxUDayk4PZy6wQMzGeGKnhcMMZTyaq2g/QtGfrvy7q1Lw2fB1VFlVblvqhoJa
|
||||
nMJojjC4zgjBkXMHsRLeTmgUKyGs+fdFbfI6uejBnnf+eMVUMIdJ+6I9AoGBANCn
|
||||
kWO9640dttyXURxNJ3lBr2H3dJOkmD6XS+u+LWqCSKQe691Y/fZ/ZL0Oc4Mhy7I6
|
||||
hsy3kDQ5k2V0fkaNODQIFJvUqXw2pMewUk8hHc9403f4fe9cPrL12rQ8WlQw4yoC
|
||||
v2B61vNczCCUDtGxlAaw8jzSRaSI5s6ax3K7enbdAoGBAJB1WYDfA2CoAQO6y9Sl
|
||||
b07A/7kQ8SN5DbPaqrDrBdJziBQxukoMJQXJeGFNUFD/DXFU5Fp2R7C86vXT7HIR
|
||||
v6m66zH+CYzOx/YE6EsUJms6UP9VIVF0Rg/RU7teXQwM01ZV32LQ8mswhTH20o/3
|
||||
uqMHmxUMEhZpUMhrfq0isyApAoGAe1UxGTXfj9AqkIVYylPIq2HqGww7+jFmVEj1
|
||||
9Wi6S6Sq72ffnzzFEPkIQL/UA4TsdHMnzsYKFPSbbXLIWUeMGyVTmTDA5c0e5XIR
|
||||
lPhMOKCAzv8w4VUzMnEkTzkFY5JqFCD/ojW57KvDdNZPVB+VEcdxyAW6aKELXMAc
|
||||
eHLc1nkCgYEApm/motCTPN32nINZ+Vvywbv64ZD+gtpeMNP3CLrbe1X9O+H52AXa
|
||||
1jCoOldWR8i2bs2NVPcKZgdo6fFULqE4dBX7Te/uYEIuuZhYLNzRO1IKU/YaqsXG
|
||||
3bfQ8hKYcSnTfE0gPtLDnqCIxTocaGLSHeG3TH9fTw+dA8FvWpUztI4=
|
||||
-----END RSA PRIVATE KEY-----
|
||||
`
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"iso_checksum": "foo",
|
||||
@@ -52,6 +82,7 @@ func TestBuilderPrepare_BootWait(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["boot_wait"] = "5s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -71,6 +102,7 @@ func TestBuilderPrepare_ISOChecksum(t *testing.T) {
|
||||
|
||||
// Test good
|
||||
config["iso_checksum"] = "FOo"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -94,6 +126,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
|
||||
|
||||
// Test good
|
||||
config["iso_checksum_type"] = "mD5"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -105,6 +138,7 @@ func TestBuilderPrepare_ISOChecksumType(t *testing.T) {
|
||||
|
||||
// Test unknown
|
||||
config["iso_checksum_type"] = "fake"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -202,6 +236,7 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
|
||||
// Bad
|
||||
config["http_port_min"] = -500
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -210,6 +245,7 @@ func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
// Good
|
||||
config["http_port_min"] = 500
|
||||
config["http_port_max"] = 1000
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -231,18 +267,59 @@ func TestBuilderPrepare_InvalidKey(t *testing.T) {
|
||||
func TestBuilderPrepare_ISOUrl(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
delete(config, "iso_url")
|
||||
delete(config, "iso_urls")
|
||||
|
||||
// Test both epty
|
||||
config["iso_url"] = ""
|
||||
b = Builder{}
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test iso_url set
|
||||
config["iso_url"] = "http://www.packer.io"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"http://www.packer.io"}
|
||||
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||
}
|
||||
|
||||
// Test both set
|
||||
config["iso_url"] = "http://www.packer.io"
|
||||
config["iso_urls"] = []string{"http://www.packer.io"}
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test just iso_urls set
|
||||
delete(config, "iso_url")
|
||||
config["iso_urls"] = []string{
|
||||
"http://www.packer.io",
|
||||
"http://www.hashicorp.com",
|
||||
}
|
||||
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
expected = []string{
|
||||
"http://www.packer.io",
|
||||
"http://www.hashicorp.com",
|
||||
}
|
||||
if !reflect.DeepEqual(b.config.ISOUrls, expected) {
|
||||
t.Fatalf("bad: %#v", b.config.ISOUrls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
@@ -257,6 +334,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
config["output_directory"] = dir
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -264,6 +342,7 @@ func TestBuilderPrepare_OutputDir(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["output_directory"] = "i-hope-i-dont-exist"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -283,12 +362,62 @@ func TestBuilderPrepare_ShutdownTimeout(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["shutdown_timeout"] = "5s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_sshKeyPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
config["ssh_key_path"] = ""
|
||||
b = Builder{}
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
config["ssh_key_path"] = "/i/dont/exist"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test bad contents
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
defer tf.Close()
|
||||
|
||||
if _, err := tf.Write([]byte("HELLO!")); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
config["ssh_key_path"] = tf.Name()
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test good contents
|
||||
tf.Seek(0, 0)
|
||||
tf.Truncate(0)
|
||||
tf.Write([]byte(testPem))
|
||||
config["ssh_key_path"] = tf.Name()
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SSHUser(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
@@ -300,6 +429,7 @@ func TestBuilderPrepare_SSHUser(t *testing.T) {
|
||||
}
|
||||
|
||||
config["ssh_username"] = "exists"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -347,6 +477,7 @@ func TestBuilderPrepare_SSHWaitTimeout(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["ssh_wait_timeout"] = "5s"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
@@ -370,6 +501,7 @@ func TestBuilderPrepare_ToolsUploadPath(t *testing.T) {
|
||||
|
||||
// Test with a bad value
|
||||
config["tools_upload_path"] = "{{{nope}"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -377,12 +509,63 @@ func TestBuilderPrepare_ToolsUploadPath(t *testing.T) {
|
||||
|
||||
// Test with a good one
|
||||
config["tools_upload_path"] = "hey"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_VMXTemplatePath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
// Test bad
|
||||
config["vmx_template_path"] = "/i/dont/exist/forreal"
|
||||
err := b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test good
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
defer tf.Close()
|
||||
|
||||
if _, err := tf.Write([]byte("HELLO!")); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
config["vmx_template_path"] = tf.Name()
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
// Bad template
|
||||
tf2, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.Remove(tf2.Name())
|
||||
defer tf2.Close()
|
||||
|
||||
if _, err := tf2.Write([]byte("{{foo}")); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
config["vmx_template_path"] = tf2.Name()
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_VNCPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
@@ -397,6 +580,7 @@ func TestBuilderPrepare_VNCPort(t *testing.T) {
|
||||
|
||||
// Bad
|
||||
config["vnc_port_min"] = -500
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
@@ -405,6 +589,7 @@ func TestBuilderPrepare_VNCPort(t *testing.T) {
|
||||
// Good
|
||||
config["vnc_port_min"] = 500
|
||||
config["vnc_port_max"] = 1000
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
|
||||
@@ -15,7 +15,7 @@ type Driver interface {
|
||||
CompactDisk(string) error
|
||||
|
||||
// CreateDisk creates a virtual disk with the given size.
|
||||
CreateDisk(string, string) error
|
||||
CreateDisk(string, string, string) error
|
||||
|
||||
// Checks if the VMX file at the given path is running.
|
||||
IsRunning(string) (bool, error)
|
||||
|
||||
@@ -28,8 +28,8 @@ func (d *Fusion5Driver) CompactDisk(diskPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
|
||||
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
|
||||
func (d *Fusion5Driver) CreateDisk(output string, size string, type_id string) error {
|
||||
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
|
||||
if _, _, err := runAndLog(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -51,12 +51,12 @@ func (d *Player5LinuxDriver) qemuCompactDisk(diskPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Player5LinuxDriver) CreateDisk(output string, size string) error {
|
||||
func (d *Player5LinuxDriver) CreateDisk(output string, size string, type_id string) error {
|
||||
var cmd *exec.Cmd
|
||||
if d.QemuImgPath != "" {
|
||||
cmd = exec.Command(d.QemuImgPath, "create", "-f", "vmdk", "-o", "compat6", output, size)
|
||||
} else {
|
||||
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
|
||||
cmd = exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
|
||||
}
|
||||
if _, _, err := runAndLog(cmd); err != nil {
|
||||
return err
|
||||
|
||||
@@ -31,8 +31,8 @@ func (d *Workstation9Driver) CompactDisk(diskPath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Workstation9Driver) CreateDisk(output string, size string) error {
|
||||
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
|
||||
func (d *Workstation9Driver) CreateDisk(output string, size string, type_id string) error {
|
||||
cmd := exec.Command(d.VdiskManagerPath, "-c", "-s", size, "-a", "lsilogic", "-t", type_id, output)
|
||||
if _, _, err := runAndLog(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+36
-5
@@ -61,12 +61,43 @@ func sshAddress(state map[string]interface{}) (string, error) {
|
||||
func sshConfig(state map[string]interface{}) (*gossh.ClientConfig, error) {
|
||||
config := state["config"].(*config)
|
||||
|
||||
auth := []gossh.ClientAuth{
|
||||
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||
gossh.ClientAuthKeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
}
|
||||
|
||||
if config.SSHKeyPath != "" {
|
||||
keyring, err := sshKeyToKeyring(config.SSHKeyPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
auth = append(auth, gossh.ClientAuthKeyring(keyring))
|
||||
}
|
||||
|
||||
return &gossh.ClientConfig{
|
||||
User: config.SSHUser,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||
gossh.ClientAuthKeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
},
|
||||
Auth: auth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sshKeyToKeyring(path string) (gossh.ClientKeyring, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
keyBytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
keyring := new(ssh.SimpleKeychain)
|
||||
if err := keyring.AddPEMKey(string(keyBytes)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return keyring, nil
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ func (stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction {
|
||||
|
||||
ui.Say("Creating virtual machine disk")
|
||||
full_disk_path := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
|
||||
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize)); err != nil {
|
||||
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize), config.DiskTypeId); err != nil {
|
||||
err := fmt.Errorf("Error creating disk: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type vmxTemplateData struct {
|
||||
@@ -42,11 +42,37 @@ func (stepCreateVMX) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ISOPath: isoPath,
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("vmx").Parse(DefaultVMXTemplate))
|
||||
t.Execute(&buf, tplData)
|
||||
vmxTemplate := DefaultVMXTemplate
|
||||
if config.VMXTemplatePath != "" {
|
||||
f, err := os.Open(config.VMXTemplatePath)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error reading VMX template: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
vmxData := ParseVMX(buf.String())
|
||||
rawBytes, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error reading VMX template: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
vmxTemplate = string(rawBytes)
|
||||
}
|
||||
|
||||
vmxContents, err := config.tpl.Process(vmxTemplate, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error procesing VMX template: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
vmxData := ParseVMX(vmxContents)
|
||||
if config.VMXData != nil {
|
||||
log.Println("Setting custom VMX data...")
|
||||
for k, v := range config.VMXData {
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This step downloads the ISO specified.
|
||||
//
|
||||
// Uses:
|
||||
// cache packer.Cache
|
||||
// config *config
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
// iso_path string
|
||||
type stepDownloadISO struct{}
|
||||
|
||||
func (s stepDownloadISO) Run(state map[string]interface{}) multistep.StepAction {
|
||||
cache := state["cache"].(packer.Cache)
|
||||
config := state["config"].(*config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
checksum, err := hex.DecodeString(config.ISOChecksum)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error parsing checksum: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
log.Printf("Acquiring lock to download the ISO.")
|
||||
cachePath := cache.Lock(config.ISOUrl)
|
||||
defer cache.Unlock(config.ISOUrl)
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: config.ISOUrl,
|
||||
TargetPath: cachePath,
|
||||
CopyFile: false,
|
||||
Hash: common.HashForType(config.ISOChecksumType),
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
|
||||
downloadCompleteCh := make(chan error, 1)
|
||||
go func() {
|
||||
ui.Say("Copying or downloading ISO. Progress will be reported periodically.")
|
||||
cachePath, err = download.Get()
|
||||
downloadCompleteCh <- err
|
||||
}()
|
||||
|
||||
progressTicker := time.NewTicker(5 * time.Second)
|
||||
defer progressTicker.Stop()
|
||||
|
||||
DownloadWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error downloading ISO: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
break DownloadWaitLoop
|
||||
case <-progressTicker.C:
|
||||
ui.Say(fmt.Sprintf("Download progress: %d%%", download.PercentProgress()))
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Path to ISO on disk: %s", cachePath)
|
||||
state["iso_path"] = cachePath
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (stepDownloadISO) Cleanup(map[string]interface{}) {}
|
||||
@@ -96,6 +96,12 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Check for interrupts between typing things so we can cancel
|
||||
// since this isn't the fastest thing.
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
vncSendString(c, command)
|
||||
}
|
||||
|
||||
|
||||
@@ -187,8 +187,18 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
env.Ui().Machine("error-count", strconv.FormatInt(int64(len(errors)), 10))
|
||||
|
||||
env.Ui().Error("\n==> Some builds didn't complete successfully and had errors:")
|
||||
for name, err := range errors {
|
||||
// Create a UI for the machine readable stuff to be targetted
|
||||
ui := &packer.TargettedUi{
|
||||
Target: name,
|
||||
Ui: env.Ui(),
|
||||
}
|
||||
|
||||
ui.Machine("error", err.Error())
|
||||
|
||||
env.Ui().Error(fmt.Sprintf("--> %s: %s", name, err))
|
||||
}
|
||||
}
|
||||
@@ -233,6 +243,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
ui.Machine("artifact", iStr, "nil")
|
||||
}
|
||||
|
||||
ui.Machine("artifact", iStr, "end")
|
||||
env.Ui().Say(message.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Command struct{}
|
||||
|
||||
func (Command) Help() string {
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
||||
func (c Command) Synopsis() string {
|
||||
return "see components of a template"
|
||||
}
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
flags := flag.NewFlagSet("inspect", flag.ContinueOnError)
|
||||
flags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
args = flags.Args()
|
||||
if len(args) != 1 {
|
||||
flags.Usage()
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read the file into a byte array so that we can parse the template
|
||||
log.Printf("Reading template: %#v", args[0])
|
||||
tpl, err := packer.ParseTemplateFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Convenience...
|
||||
ui := env.Ui()
|
||||
|
||||
// Variables
|
||||
ui.Say("Variables and their defaults:\n")
|
||||
if len(tpl.Variables) == 0 {
|
||||
ui.Say(" <No variables>")
|
||||
} else {
|
||||
keys := make([]string, 0, len(tpl.Variables))
|
||||
max := 0
|
||||
for k, _ := range tpl.Variables {
|
||||
keys = append(keys, k)
|
||||
if len(k) > max {
|
||||
max = len(k)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := tpl.Variables[k]
|
||||
padding := strings.Repeat(" ", max-len(k))
|
||||
output := fmt.Sprintf(" %s%s = %s", k, padding, v)
|
||||
|
||||
ui.Machine("template-variable", k, v)
|
||||
ui.Say(output)
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("")
|
||||
|
||||
// Builders
|
||||
ui.Say("Builders:\n")
|
||||
if len(tpl.Builders) == 0 {
|
||||
ui.Say(" <No builders>")
|
||||
} else {
|
||||
keys := make([]string, 0, len(tpl.Builders))
|
||||
max := 0
|
||||
for k, _ := range tpl.Builders {
|
||||
keys = append(keys, k)
|
||||
if len(k) > max {
|
||||
max = len(k)
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, k := range keys {
|
||||
v := tpl.Builders[k]
|
||||
padding := strings.Repeat(" ", max-len(k))
|
||||
output := fmt.Sprintf(" %s%s", k, padding)
|
||||
if v.Name != v.Type {
|
||||
output = fmt.Sprintf("%s (%s)", output, v.Type)
|
||||
}
|
||||
|
||||
ui.Machine("template-builder", k, v.Type)
|
||||
ui.Say(output)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say("")
|
||||
|
||||
// Provisioners
|
||||
ui.Say("Provisioners:\n")
|
||||
if len(tpl.Provisioners) == 0 {
|
||||
ui.Say(" <No provisioners>")
|
||||
} else {
|
||||
for _, v := range tpl.Provisioners {
|
||||
ui.Machine("template-provisioner", v.Type)
|
||||
ui.Say(fmt.Sprintf(" %s", v.Type))
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommand_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(Command)
|
||||
if _, ok := raw.(packer.Command); !ok {
|
||||
t.Fatalf("must be a Command")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package inspect
|
||||
|
||||
const helpText = `
|
||||
Usage: packer inspect TEMPLATE
|
||||
|
||||
Inspects a template, parsing and outputting the components a template
|
||||
defines. This does not validate the contents of a template (other than
|
||||
basic syntax by necessity).
|
||||
|
||||
Options:
|
||||
|
||||
-machine-readable Machine-readable output
|
||||
`
|
||||
@@ -65,6 +65,27 @@ func (f *BuildOptions) AllUserVars() (map[string]string, error) {
|
||||
// configured options.
|
||||
func (f *BuildOptions) Builds(t *packer.Template, cf *packer.ComponentFinder) ([]packer.Build, error) {
|
||||
buildNames := t.BuildNames()
|
||||
|
||||
checks := make(map[string][]string)
|
||||
checks["except"] = f.Except
|
||||
checks["only"] = f.Only
|
||||
for t, ns := range checks {
|
||||
for _, n := range ns {
|
||||
found := false
|
||||
for _, actual := range buildNames {
|
||||
if actual == n {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, fmt.Errorf(
|
||||
"Unknown build in '%s' flag: %s", t, n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
if len(f.Except) > 0 {
|
||||
|
||||
@@ -1,9 +1,102 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testTemplate() (*packer.Template, *packer.ComponentFinder) {
|
||||
tplData := `{
|
||||
"builders": [
|
||||
{
|
||||
"type": "foo"
|
||||
},
|
||||
{
|
||||
"type": "bar"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
tpl, err := packer.ParseTemplate([]byte(tplData))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
cf := &packer.ComponentFinder{
|
||||
Builder: func(string) (packer.Builder, error) { return new(packer.MockBuilder), nil },
|
||||
}
|
||||
|
||||
return tpl, cf
|
||||
}
|
||||
|
||||
func TestBuildOptionsBuilds(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
bs, err := opts.Builds(testTemplate())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(bs) != 2 {
|
||||
t.Fatalf("bad: %d", len(bs))
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsBuilds_except(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
opts.Except = []string{"foo"}
|
||||
|
||||
bs, err := opts.Builds(testTemplate())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(bs) != 1 {
|
||||
t.Fatalf("bad: %d", len(bs))
|
||||
}
|
||||
|
||||
if bs[0].Name() != "bar" {
|
||||
t.Fatalf("bad: %s", bs[0].Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsBuilds_only(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
opts.Only = []string{"foo"}
|
||||
|
||||
bs, err := opts.Builds(testTemplate())
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(bs) != 1 {
|
||||
t.Fatalf("bad: %d", len(bs))
|
||||
}
|
||||
|
||||
if bs[0].Name() != "foo" {
|
||||
t.Fatalf("bad: %s", bs[0].Name())
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsBuilds_exceptNonExistent(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
opts.Except = []string{"i-dont-exist"}
|
||||
|
||||
_, err := opts.Builds(testTemplate())
|
||||
if err == nil {
|
||||
t.Fatal("err should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsBuilds_onlyNonExistent(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
opts.Only = []string{"i-dont-exist"}
|
||||
|
||||
_, err := opts.Builds(testTemplate())
|
||||
if err == nil {
|
||||
t.Fatal("err should not be nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsValidate(t *testing.T) {
|
||||
bf := new(BuildOptions)
|
||||
|
||||
|
||||
@@ -64,6 +64,15 @@ func DecodeConfig(target interface{}, raws ...interface{}) (*mapstructure.Metada
|
||||
// a completely valid URL. For example, the original URL might be "local/file.iso"
|
||||
// which isn't a valid URL. DownloadableURL will return "file:///local/file.iso"
|
||||
func DownloadableURL(original string) (string, error) {
|
||||
if runtime.GOOS == "windows" {
|
||||
// If the distance to the first ":" is just one character, assume
|
||||
// we're dealing with a drive letter and thus a file path.
|
||||
idx := strings.Index(original, ":")
|
||||
if idx == 1 {
|
||||
original = "file:///" + original
|
||||
}
|
||||
}
|
||||
|
||||
url, err := url.Parse(original)
|
||||
if err != nil {
|
||||
return "", err
|
||||
|
||||
+18
-4
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/md5"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/sha512"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -60,6 +61,8 @@ func HashForType(t string) hash.Hash {
|
||||
return sha1.New()
|
||||
case "sha256":
|
||||
return sha256.New()
|
||||
case "sha512":
|
||||
return sha512.New()
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
@@ -148,12 +151,12 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
}
|
||||
|
||||
// PercentProgress returns the download progress as a percentage.
|
||||
func (d *DownloadClient) PercentProgress() uint {
|
||||
func (d *DownloadClient) PercentProgress() int {
|
||||
if d.downloader == nil {
|
||||
return 0
|
||||
return -1
|
||||
}
|
||||
|
||||
return uint((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
|
||||
return int((float64(d.downloader.Progress()) / float64(d.downloader.Total())) * 100)
|
||||
}
|
||||
|
||||
// VerifyChecksum tests that the path matches the checksum for the
|
||||
@@ -188,7 +191,18 @@ func (*HTTPDownloader) Cancel() {
|
||||
|
||||
func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error {
|
||||
log.Printf("Starting download: %s", src.String())
|
||||
resp, err := http.Get(src.String())
|
||||
req, err := http.NewRequest("GET", src.String(), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpClient := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
},
|
||||
}
|
||||
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -81,6 +81,19 @@ func TestHashForType(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
if h := HashForType("sha512"); h == nil {
|
||||
t.Fatalf("sha512 hash is nil")
|
||||
} else {
|
||||
h.Write([]byte("foo"))
|
||||
result := h.Sum(nil)
|
||||
|
||||
expected := "f7fbba6e0636f890e56fbbf3283e524c6fa3204ae298382d624741d0dc6638326e282c41be5e4254d8820772c5518a2c5a8c0c7f7eda19594a7eb539453e1ed7"
|
||||
actual := hex.EncodeToString(result)
|
||||
if actual != expected {
|
||||
t.Fatalf("bad hash: %s", actual)
|
||||
}
|
||||
}
|
||||
|
||||
if HashForType("fake") != nil {
|
||||
t.Fatalf("fake hash is not nil")
|
||||
}
|
||||
|
||||
@@ -34,6 +34,9 @@ type StepConnectSSH struct {
|
||||
// SSHWaitTimeout is the total timeout to wait for SSH to become available.
|
||||
SSHWaitTimeout time.Duration
|
||||
|
||||
// NoPty, if true, will not request a Pty from the remote end.
|
||||
NoPty bool
|
||||
|
||||
comm packer.Communicator
|
||||
}
|
||||
|
||||
@@ -116,7 +119,7 @@ func (s *StepConnectSSH) waitForSSH(state map[string]interface{}, cancel <-chan
|
||||
}
|
||||
|
||||
// Attempt to connect to SSH port
|
||||
connFunc := ssh.ConnectFunc("tcp", address, 5*time.Minute)
|
||||
connFunc := ssh.ConnectFunc("tcp", address)
|
||||
nc, err := connFunc()
|
||||
if err != nil {
|
||||
log.Printf("TCP connection to SSH ip/port failed: %s", err)
|
||||
@@ -128,6 +131,7 @@ func (s *StepConnectSSH) waitForSSH(state map[string]interface{}, cancel <-chan
|
||||
config := &ssh.Config{
|
||||
Connection: connFunc,
|
||||
SSHConfig: sshConfig,
|
||||
NoPty: s.NoPty,
|
||||
}
|
||||
|
||||
log.Println("Attempting SSH connection...")
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StepDownload downloads a remote file using the download client within
|
||||
// this package. This step handles setting up the download configuration,
|
||||
// progress reporting, interrupt handling, etc.
|
||||
//
|
||||
// Uses:
|
||||
// cache packer.Cache
|
||||
// ui packer.Ui
|
||||
type StepDownload struct {
|
||||
// The checksum and the type of the checksum for the download
|
||||
Checksum string
|
||||
ChecksumType string
|
||||
|
||||
// A short description of the type of download being done. Example:
|
||||
// "ISO" or "Guest Additions"
|
||||
Description string
|
||||
|
||||
// The name of the key where the final path of the ISO will be put
|
||||
// into the state.
|
||||
ResultKey string
|
||||
|
||||
// The path where the result should go, otherwise it goes to the
|
||||
// cache directory.
|
||||
TargetPath string
|
||||
|
||||
// A list of URLs to attempt to download this thing.
|
||||
Url []string
|
||||
}
|
||||
|
||||
func (s *StepDownload) Run(state map[string]interface{}) multistep.StepAction {
|
||||
cache := state["cache"].(packer.Cache)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
var checksum []byte
|
||||
if s.Checksum != "" {
|
||||
var err error
|
||||
checksum, err = hex.DecodeString(s.Checksum)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error parsing checksum: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
ui.Say(fmt.Sprintf("Downloading or copying %s", s.Description))
|
||||
|
||||
var finalPath string
|
||||
for _, url := range s.Url {
|
||||
ui.Message(fmt.Sprintf("Downloading or copying: %s", url))
|
||||
|
||||
targetPath := s.TargetPath
|
||||
if targetPath == "" {
|
||||
log.Printf("Acquiring lock to download: %s", url)
|
||||
targetPath = cache.Lock(url)
|
||||
defer cache.Unlock(url)
|
||||
}
|
||||
|
||||
config := &DownloadConfig{
|
||||
Url: url,
|
||||
TargetPath: targetPath,
|
||||
CopyFile: false,
|
||||
Hash: HashForType(s.ChecksumType),
|
||||
Checksum: checksum,
|
||||
}
|
||||
|
||||
path, err, retry := s.download(config, state)
|
||||
if err != nil {
|
||||
ui.Message(fmt.Sprintf("Error downloading: %s", err))
|
||||
}
|
||||
|
||||
if !retry {
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
finalPath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if finalPath == "" {
|
||||
err := fmt.Errorf("%s download failed.", s.Description)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state[s.ResultKey] = finalPath
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepDownload) Cleanup(map[string]interface{}) {}
|
||||
|
||||
func (s *StepDownload) download(config *DownloadConfig, state map[string]interface{}) (string, error, bool) {
|
||||
var path string
|
||||
ui := state["ui"].(packer.Ui)
|
||||
download := NewDownloadClient(config)
|
||||
|
||||
downloadCompleteCh := make(chan error, 1)
|
||||
go func() {
|
||||
var err error
|
||||
path, err = download.Get()
|
||||
downloadCompleteCh <- err
|
||||
}()
|
||||
|
||||
progressTicker := time.NewTicker(5 * time.Second)
|
||||
defer progressTicker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case err := <-downloadCompleteCh:
|
||||
if err != nil {
|
||||
return "", err, true
|
||||
}
|
||||
|
||||
return path, nil, true
|
||||
case <-progressTicker.C:
|
||||
progress := download.PercentProgress()
|
||||
if progress >= 0 {
|
||||
ui.Message(fmt.Sprintf("Download progress: %d%%", progress))
|
||||
}
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
ui.Say("Interrupt received. Cancelling download...")
|
||||
return "", nil, false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStepDownload_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(StepDownload)
|
||||
if _, ok := raw.(multistep.Step); !ok {
|
||||
t.Fatalf("download should be a step")
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,10 @@ import (
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type comm struct {
|
||||
@@ -28,6 +31,9 @@ type Config struct {
|
||||
// in use will be closed as part of the Close method, or in the
|
||||
// case an error occurs.
|
||||
Connection func() (net.Conn, error)
|
||||
|
||||
// NoPty, if true, will not request a pty from the remote end.
|
||||
NoPty bool
|
||||
}
|
||||
|
||||
// Creates a new packer.Communicator implementation over SSH. This takes
|
||||
@@ -57,15 +63,17 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
|
||||
session.Stdout = cmd.Stdout
|
||||
session.Stderr = cmd.Stderr
|
||||
|
||||
// Request a PTY
|
||||
termModes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0, // do not echo
|
||||
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||
}
|
||||
if !c.config.NoPty {
|
||||
// Request a PTY
|
||||
termModes := ssh.TerminalModes{
|
||||
ssh.ECHO: 0, // do not echo
|
||||
ssh.TTY_OP_ISPEED: 14400, // input speed = 14.4kbaud
|
||||
ssh.TTY_OP_OSPEED: 14400, // output speed = 14.4kbaud
|
||||
}
|
||||
|
||||
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
|
||||
return
|
||||
if err = session.RequestPty("xterm", 80, 40, termModes); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("starting remote command: %s", cmd.Command)
|
||||
@@ -74,10 +82,16 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// A channel to keep track of our done state
|
||||
doneCh := make(chan struct{})
|
||||
sessionLock := new(sync.Mutex)
|
||||
timedOut := false
|
||||
|
||||
// Start a goroutine to wait for the session to end and set the
|
||||
// exit boolean and status.
|
||||
go func() {
|
||||
defer session.Close()
|
||||
|
||||
err := session.Wait()
|
||||
exitStatus := 0
|
||||
if err != nil {
|
||||
@@ -87,46 +101,61 @@ func (c *comm) Start(cmd *packer.RemoteCmd) (err error) {
|
||||
}
|
||||
}
|
||||
|
||||
sessionLock.Lock()
|
||||
defer sessionLock.Unlock()
|
||||
|
||||
if timedOut {
|
||||
// We timed out, so set the exit status to -1
|
||||
exitStatus = -1
|
||||
}
|
||||
|
||||
log.Printf("remote command exited with '%d': %s", exitStatus, cmd.Command)
|
||||
cmd.SetExited(exitStatus)
|
||||
close(doneCh)
|
||||
}()
|
||||
|
||||
go func() {
|
||||
failures := 0
|
||||
for {
|
||||
dummy, err := c.config.Connection()
|
||||
if err == nil {
|
||||
failures = 0
|
||||
dummy.Close()
|
||||
}
|
||||
|
||||
select {
|
||||
case <-doneCh:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("background SSH connection checker failure: %s", err)
|
||||
failures += 1
|
||||
}
|
||||
|
||||
if failures < 5 {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Acquire a lock in order to modify session state
|
||||
sessionLock.Lock()
|
||||
defer sessionLock.Unlock()
|
||||
|
||||
// Kill the connection and mark that we timed out.
|
||||
log.Printf("Too many SSH connection failures. Killing it!")
|
||||
c.conn.Close()
|
||||
timedOut = true
|
||||
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *comm) Upload(path string, input io.Reader) error {
|
||||
session, err := c.newSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer session.Close()
|
||||
|
||||
// Get a pipe to stdin so that we can send data down
|
||||
w, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We only want to close once, so we nil w after we close it,
|
||||
// and only close in the defer if it hasn't been closed already.
|
||||
defer func() {
|
||||
if w != nil {
|
||||
w.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get a pipe to stdout so that we can get responses back
|
||||
stdoutPipe, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdoutR := bufio.NewReader(stdoutPipe)
|
||||
|
||||
// Set stderr to a bytes buffer
|
||||
stderr := new(bytes.Buffer)
|
||||
session.Stderr = stderr
|
||||
|
||||
// The target directory and file for talking the SCP protocol
|
||||
target_dir := filepath.Dir(path)
|
||||
target_file := filepath.Base(path)
|
||||
@@ -136,69 +165,41 @@ func (c *comm) Upload(path string, input io.Reader) error {
|
||||
// which works for unix and windows
|
||||
target_dir = filepath.ToSlash(target_dir)
|
||||
|
||||
// Start the sink mode on the other side
|
||||
// TODO(mitchellh): There are probably issues with shell escaping the path
|
||||
log.Println("Starting remote scp process in sink mode")
|
||||
if err = session.Start("scp -vt " + target_dir); err != nil {
|
||||
return err
|
||||
scpFunc := func(w io.Writer, stdoutR *bufio.Reader) error {
|
||||
return scpUploadFile(target_file, input, w, stdoutR)
|
||||
}
|
||||
|
||||
// Determine the length of the upload content by copying it
|
||||
// into an in-memory buffer. Note that this means what we upload
|
||||
// must fit into memory.
|
||||
log.Println("Copying input data into in-memory buffer so we can get the length")
|
||||
input_memory := new(bytes.Buffer)
|
||||
if _, err = io.Copy(input_memory, input); err != nil {
|
||||
return err
|
||||
}
|
||||
return c.scpSession("scp -vt "+target_dir, scpFunc)
|
||||
}
|
||||
|
||||
// Start the protocol
|
||||
log.Println("Beginning file upload...")
|
||||
fmt.Fprintln(w, "C0644", input_memory.Len(), target_file)
|
||||
err = checkSCPStatus(stdoutR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
io.Copy(w, input_memory)
|
||||
fmt.Fprint(w, "\x00")
|
||||
err = checkSCPStatus(stdoutR)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close the stdin, which sends an EOF, and then set w to nil so that
|
||||
// our defer func doesn't close it again since that is unsafe with
|
||||
// the Go SSH package.
|
||||
log.Println("Upload complete, closing stdin pipe")
|
||||
w.Close()
|
||||
w = nil
|
||||
|
||||
// Wait for the SCP connection to close, meaning it has consumed all
|
||||
// our data and has completed. Or has errored.
|
||||
log.Println("Waiting for SSH session to complete")
|
||||
err = session.Wait()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*ssh.ExitError); ok {
|
||||
// Otherwise, we have an ExitErorr, meaning we can just read
|
||||
// the exit status
|
||||
log.Printf("non-zero exit status: %d", exitErr.ExitStatus())
|
||||
|
||||
// If we exited with status 127, it means SCP isn't available.
|
||||
// Return a more descriptive error for that.
|
||||
if exitErr.ExitStatus() == 127 {
|
||||
return errors.New(
|
||||
"SCP failed to start. This usually means that SCP is not\n" +
|
||||
"properly installed on the remote system.")
|
||||
func (c *comm) UploadDir(dst string, src string, excl []string) error {
|
||||
log.Printf("Upload dir '%s' to '%s'", src, dst)
|
||||
scpFunc := func(w io.Writer, r *bufio.Reader) error {
|
||||
uploadEntries := func() error {
|
||||
f, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
entries, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scpUploadDir(src, entries, w, r)
|
||||
}
|
||||
|
||||
return err
|
||||
if src[len(src)-1] != '/' {
|
||||
log.Printf("No trailing slash, creating the source directory name")
|
||||
return scpUploadDirProtocol(filepath.Base(src), w, r, uploadEntries)
|
||||
} else {
|
||||
// Trailing slash, so only upload the contents
|
||||
return uploadEntries()
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
|
||||
|
||||
return nil
|
||||
return c.scpSession("scp -rvt "+dst, scpFunc)
|
||||
}
|
||||
|
||||
func (c *comm) Download(string, io.Writer) error {
|
||||
@@ -250,6 +251,84 @@ func (c *comm) reconnect() (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *comm) scpSession(scpCommand string, f func(io.Writer, *bufio.Reader) error) error {
|
||||
session, err := c.newSession()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Get a pipe to stdin so that we can send data down
|
||||
stdinW, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We only want to close once, so we nil w after we close it,
|
||||
// and only close in the defer if it hasn't been closed already.
|
||||
defer func() {
|
||||
if stdinW != nil {
|
||||
stdinW.Close()
|
||||
}
|
||||
}()
|
||||
|
||||
// Get a pipe to stdout so that we can get responses back
|
||||
stdoutPipe, err := session.StdoutPipe()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdoutR := bufio.NewReader(stdoutPipe)
|
||||
|
||||
// Set stderr to a bytes buffer
|
||||
stderr := new(bytes.Buffer)
|
||||
session.Stderr = stderr
|
||||
|
||||
// Start the sink mode on the other side
|
||||
// TODO(mitchellh): There are probably issues with shell escaping the path
|
||||
log.Println("Starting remote scp process: %s", scpCommand)
|
||||
if err := session.Start(scpCommand); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Call our callback that executes in the context of SCP
|
||||
log.Println("Started SCP session, beginning transfers...")
|
||||
if err := f(stdinW, stdoutR); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Close the stdin, which sends an EOF, and then set w to nil so that
|
||||
// our defer func doesn't close it again since that is unsafe with
|
||||
// the Go SSH package.
|
||||
log.Println("SCP session complete, closing stdin pipe.")
|
||||
stdinW.Close()
|
||||
stdinW = nil
|
||||
|
||||
// Wait for the SCP connection to close, meaning it has consumed all
|
||||
// our data and has completed. Or has errored.
|
||||
log.Println("Waiting for SSH session to complete.")
|
||||
err = session.Wait()
|
||||
if err != nil {
|
||||
if exitErr, ok := err.(*ssh.ExitError); ok {
|
||||
// Otherwise, we have an ExitErorr, meaning we can just read
|
||||
// the exit status
|
||||
log.Printf("non-zero exit status: %d", exitErr.ExitStatus())
|
||||
|
||||
// If we exited with status 127, it means SCP isn't available.
|
||||
// Return a more descriptive error for that.
|
||||
if exitErr.ExitStatus() == 127 {
|
||||
return errors.New(
|
||||
"SCP failed to start. This usually means that SCP is not\n" +
|
||||
"properly installed on the remote system.")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
log.Printf("scp stderr (length %d): %s", stderr.Len(), stderr.String())
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkSCPStatus checks that a prior command sent to SCP completed
|
||||
// successfully. If it did not complete successfully, an error will
|
||||
// be returned.
|
||||
@@ -271,3 +350,100 @@ func checkSCPStatus(r *bufio.Reader) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scpUploadFile(dst string, src io.Reader, w io.Writer, r *bufio.Reader) error {
|
||||
// Determine the length of the upload content by copying it
|
||||
// into an in-memory buffer. Note that this means what we upload
|
||||
// must fit into memory.
|
||||
log.Println("Copying input data into in-memory buffer so we can get the length")
|
||||
inputBuf := new(bytes.Buffer)
|
||||
if _, err := io.Copy(inputBuf, src); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Start the protocol
|
||||
log.Println("Beginning file upload...")
|
||||
fmt.Fprintln(w, "C0644", inputBuf.Len(), dst)
|
||||
err := checkSCPStatus(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := io.Copy(w, inputBuf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprint(w, "\x00")
|
||||
err = checkSCPStatus(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scpUploadDirProtocol(name string, w io.Writer, r *bufio.Reader, f func() error) error {
|
||||
log.Printf("SCP: starting directory upload: %s", name)
|
||||
fmt.Fprintln(w, "D0755 0", name)
|
||||
err := checkSCPStatus(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := f(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "E")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func scpUploadDir(root string, fs []os.FileInfo, w io.Writer, r *bufio.Reader) error {
|
||||
for _, fi := range fs {
|
||||
realPath := filepath.Join(root, fi.Name())
|
||||
|
||||
if !fi.IsDir() {
|
||||
// It is a regular file, just upload it
|
||||
f, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = func() error {
|
||||
defer f.Close()
|
||||
return scpUploadFile(fi.Name(), f, w, r)
|
||||
}()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// It is a directory, recursively upload
|
||||
err := scpUploadDirProtocol(fi.Name(), w, r, func() error {
|
||||
f, err := os.Open(realPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
entries, err := f.Readdir(-1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return scpUploadDir(realPath, entries, w, r)
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !race
|
||||
|
||||
package ssh
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package ssh
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"time"
|
||||
@@ -10,24 +9,9 @@ import (
|
||||
// ConnectFunc is a convenience method for returning a function
|
||||
// that just uses net.Dial to communicate with the remote end that
|
||||
// is suitable for use with the SSH communicator configuration.
|
||||
func ConnectFunc(network, addr string, timeout time.Duration) func() (net.Conn, error) {
|
||||
func ConnectFunc(network, addr string) func() (net.Conn, error) {
|
||||
return func() (net.Conn, error) {
|
||||
timeoutCh := time.After(timeout)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timeoutCh:
|
||||
return nil, errors.New("timeout connecting to remote machine")
|
||||
default:
|
||||
}
|
||||
|
||||
log.Printf("Opening conn for SSH to %s %s", network, addr)
|
||||
nc, err := net.DialTimeout(network, addr, 15*time.Second)
|
||||
if err == nil {
|
||||
return nc, nil
|
||||
}
|
||||
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
log.Printf("Opening conn for SSH to %s %s", network, addr)
|
||||
return net.DialTimeout(network, addr, 15*time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,10 @@ type SimpleKeychain struct {
|
||||
// AddPEMKey adds a simple PEM encoded private key to the keychain.
|
||||
func (k *SimpleKeychain) AddPEMKey(key string) (err error) {
|
||||
block, _ := pem.Decode([]byte(key))
|
||||
if block == nil {
|
||||
return errors.New("no block in key")
|
||||
}
|
||||
|
||||
rsakey, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return
|
||||
|
||||
@@ -23,6 +23,7 @@ const defaultConfig = `
|
||||
"amazon-chroot": "packer-builder-amazon-chroot",
|
||||
"amazon-instance": "packer-builder-amazon-instance",
|
||||
"digitalocean": "packer-builder-digitalocean",
|
||||
"openstack": "packer-builder-openstack",
|
||||
"virtualbox": "packer-builder-virtualbox",
|
||||
"vmware": "packer-builder-vmware"
|
||||
},
|
||||
@@ -30,6 +31,7 @@ const defaultConfig = `
|
||||
"commands": {
|
||||
"build": "packer-command-build",
|
||||
"fix": "packer-command-fix",
|
||||
"inspect": "packer-command-inspect",
|
||||
"validate": "packer-command-validate"
|
||||
},
|
||||
|
||||
@@ -38,6 +40,7 @@ const defaultConfig = `
|
||||
},
|
||||
|
||||
"provisioners": {
|
||||
"chef-solo": "packer-provisioner-chef-solo",
|
||||
"file": "packer-provisioner-file",
|
||||
"shell": "packer-provisioner-shell",
|
||||
"salt-masterless": "packer-provisioner-salt-masterless"
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
"github.com/mitchellh/panicwrap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -15,32 +16,68 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Setup logging if PACKER_LOG is set.
|
||||
// Log to PACKER_LOG_PATH if it is set, otherwise default to stderr.
|
||||
var logOutput io.Writer = ioutil.Discard
|
||||
if os.Getenv("PACKER_LOG") != "" {
|
||||
logOutput = os.Stderr
|
||||
|
||||
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
|
||||
var err error
|
||||
logOutput, err = os.Create(logPath)
|
||||
if err != nil {
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"Couldn't open '%s' for logging: %s",
|
||||
logPath, err)
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.SetOutput(logOutput)
|
||||
// Call realMain instead of doing the work here so we can use
|
||||
// `defer` statements within the function and have them work properly.
|
||||
// (defers aren't called with os.Exit)
|
||||
os.Exit(realMain())
|
||||
}
|
||||
|
||||
// realMain is executed from main and returns the exit status to exit with.
|
||||
func realMain() int {
|
||||
// If there is no explicit number of Go threads to use, then set it
|
||||
if os.Getenv("GOMAXPROCS") == "" {
|
||||
runtime.GOMAXPROCS(runtime.NumCPU())
|
||||
}
|
||||
|
||||
// Determine where logs should go in general (requested by the user)
|
||||
logWriter, err := logOutput()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't setup log output: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
// We also always send logs to a temporary file that we use in case
|
||||
// there is a panic. Otherwise, we delete it.
|
||||
logTempFile, err := ioutil.TempFile("", "packer-log")
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't setup logging tempfile: %s", err)
|
||||
return 1
|
||||
}
|
||||
defer os.Remove(logTempFile.Name())
|
||||
defer logTempFile.Close()
|
||||
|
||||
// Reset the log variables to minimize work in the subprocess
|
||||
os.Setenv("PACKER_LOG", "")
|
||||
os.Setenv("PACKER_LOG_FILE", "")
|
||||
|
||||
// Create the configuration for panicwrap and wrap our executable
|
||||
wrapConfig := &panicwrap.WrapConfig{
|
||||
Handler: panicHandler(logTempFile),
|
||||
Writer: io.MultiWriter(logTempFile, logWriter),
|
||||
}
|
||||
|
||||
exitStatus, err := panicwrap.Wrap(wrapConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Couldn't start Packer: %s", err)
|
||||
return 1
|
||||
}
|
||||
|
||||
if exitStatus >= 0 {
|
||||
return exitStatus
|
||||
}
|
||||
|
||||
// We're the child, so just close the tempfile we made in order to
|
||||
// save file handles since the tempfile is only used by the parent.
|
||||
logTempFile.Close()
|
||||
|
||||
return wrappedMain()
|
||||
}
|
||||
|
||||
// wrappedMain is called only when we're wrapped by panicwrap and
|
||||
// returns the exit status to exit with.
|
||||
func wrappedMain() int {
|
||||
log.SetOutput(os.Stderr)
|
||||
|
||||
log.Printf(
|
||||
"Packer Version: %s %s %s",
|
||||
packer.Version, packer.VersionPrerelease, packer.GitCommit)
|
||||
@@ -52,7 +89,7 @@ func main() {
|
||||
config, err := loadConfig()
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error loading configuration: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
log.Printf("Packer config: %+v", config)
|
||||
@@ -65,12 +102,12 @@ func main() {
|
||||
cacheDir, err = filepath.Abs(cacheDir)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cacheDir, 0755); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error preparing cache directory: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
log.Printf("Setting cache directory: %s", cacheDir)
|
||||
@@ -100,8 +137,7 @@ func main() {
|
||||
env, err := packer.NewEnvironment(envConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
|
||||
plugin.CleanupClients()
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
setupSignalHandlers(env)
|
||||
@@ -109,12 +145,10 @@ func main() {
|
||||
exitCode, err := env.Cli(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
|
||||
plugin.CleanupClients()
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
plugin.CleanupClients()
|
||||
os.Exit(exitCode)
|
||||
return exitCode
|
||||
}
|
||||
|
||||
// extractMachineReadable checks the args for the machine readable
|
||||
@@ -178,3 +212,21 @@ func loadConfig() (*config, error) {
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// logOutput determines where we should send logs (if anywhere).
|
||||
func logOutput() (logOutput io.Writer, err error) {
|
||||
logOutput = ioutil.Discard
|
||||
if os.Getenv("PACKER_LOG") != "" {
|
||||
logOutput = os.Stderr
|
||||
|
||||
if logPath := os.Getenv("PACKER_LOG_PATH"); logPath != "" {
|
||||
var err error
|
||||
logOutput, err = os.Create(logPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package packer
|
||||
|
||||
// MockArtifact is an implementation of Artifact that can be used for tests.
|
||||
type MockArtifact struct {
|
||||
IdValue string
|
||||
DestroyCalled bool
|
||||
}
|
||||
|
||||
func (*MockArtifact) BuilderId() string {
|
||||
return "bid"
|
||||
}
|
||||
|
||||
func (*MockArtifact) Files() []string {
|
||||
return []string{"a", "b"}
|
||||
}
|
||||
|
||||
func (a *MockArtifact) Id() string {
|
||||
id := a.IdValue
|
||||
if id == "" {
|
||||
id = "id"
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (*MockArtifact) String() string {
|
||||
return "string"
|
||||
}
|
||||
|
||||
func (a *MockArtifact) Destroy() error {
|
||||
a.DestroyCalled = true
|
||||
return nil
|
||||
}
|
||||
+1
-1
@@ -90,7 +90,7 @@ type coreBuild struct {
|
||||
type coreBuildPostProcessor struct {
|
||||
processor PostProcessor
|
||||
processorType string
|
||||
config interface{}
|
||||
config map[string]interface{}
|
||||
keepInputArtifact bool
|
||||
}
|
||||
|
||||
|
||||
+10
-10
@@ -20,7 +20,7 @@ func testBuild() *coreBuild {
|
||||
},
|
||||
postProcessors: [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", make(map[string]interface{}), true},
|
||||
},
|
||||
},
|
||||
variables: make(map[string]string),
|
||||
@@ -66,7 +66,7 @@ func TestBuild_Prepare(t *testing.T) {
|
||||
corePP := build.postProcessors[0][0]
|
||||
pp := corePP.processor.(*TestPostProcessor)
|
||||
assert.True(pp.configCalled, "config should be called")
|
||||
assert.Equal(pp.configVal, []interface{}{42, packerConfig}, "config should have right value")
|
||||
assert.Equal(pp.configVal, []interface{}{make(map[string]interface{}), packerConfig}, "config should have right value")
|
||||
}
|
||||
|
||||
func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
@@ -231,7 +231,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
build = testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "pp", make(map[string]interface{}), false},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -256,10 +256,10 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
build = testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1"}, "pp", make(map[string]interface{}), false},
|
||||
},
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2"}, "pp", 42, true},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2"}, "pp", make(map[string]interface{}), true},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -284,12 +284,12 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
build = testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1a"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1b"}, "pp", 42, true},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1a"}, "pp", make(map[string]interface{}), false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp1b"}, "pp", make(map[string]interface{}), true},
|
||||
},
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2a"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2b"}, "pp", 42, false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2a"}, "pp", make(map[string]interface{}), false},
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp2b"}, "pp", make(map[string]interface{}), false},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -315,7 +315,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
build.postProcessors = [][]coreBuildPostProcessor{
|
||||
[]coreBuildPostProcessor{
|
||||
coreBuildPostProcessor{
|
||||
&TestPostProcessor{artifactId: "pp", keep: true}, "pp", 42, false,
|
||||
&TestPostProcessor{artifactId: "pp", keep: true}, "pp", make(map[string]interface{}), false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package packer
|
||||
|
||||
// MockBuilder is an implementation of Builder that can be used for tests.
|
||||
// You can set some fake return values and you can keep track of what
|
||||
// methods were called on the builder. It is fairly basic.
|
||||
type MockBuilder struct {
|
||||
ArtifactId string
|
||||
|
||||
PrepareCalled bool
|
||||
PrepareConfig []interface{}
|
||||
RunCalled bool
|
||||
RunCache Cache
|
||||
RunHook Hook
|
||||
RunUi Ui
|
||||
CancelCalled bool
|
||||
}
|
||||
|
||||
func (tb *MockBuilder) Prepare(config ...interface{}) error {
|
||||
tb.PrepareCalled = true
|
||||
tb.PrepareConfig = config
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tb *MockBuilder) Run(ui Ui, h Hook, c Cache) (Artifact, error) {
|
||||
tb.RunCalled = true
|
||||
tb.RunHook = h
|
||||
tb.RunUi = ui
|
||||
tb.RunCache = c
|
||||
return &MockArtifact{
|
||||
IdValue: tb.ArtifactId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (tb *MockBuilder) Cancel() {
|
||||
tb.CancelCalled = true
|
||||
}
|
||||
+21
-6
@@ -33,9 +33,11 @@ type RemoteCmd struct {
|
||||
// Once Exited is true, this will contain the exit code of the process.
|
||||
ExitStatus int
|
||||
|
||||
// Internal locks and such used for safely setting some shared variables
|
||||
l sync.Mutex
|
||||
// Internal fields
|
||||
exitCh chan struct{}
|
||||
|
||||
// This thing is a mutex, lock when making modifications concurrently
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
// A Communicator is the interface used to communicate with the machine
|
||||
@@ -57,6 +59,16 @@ type Communicator interface {
|
||||
// it completes.
|
||||
Upload(string, io.Reader) error
|
||||
|
||||
// UploadDir uploads the contents of a directory recursively to
|
||||
// the remote path. It also takes an optional slice of paths to
|
||||
// ignore when uploading.
|
||||
//
|
||||
// The folder name of the source folder should be created unless there
|
||||
// is a trailing slash on the source "/". For example: "/tmp/src" as
|
||||
// the source will create a "src" directory in the destination unless
|
||||
// a trailing slash is added. This is identical behavior to rsync(1).
|
||||
UploadDir(string, string, []string) error
|
||||
|
||||
// Download downloads a file from the machine from the given remote path
|
||||
// with the contents writing to the given writer. This method will
|
||||
// block until it completes.
|
||||
@@ -76,6 +88,9 @@ func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
|
||||
originalStdout := r.Stdout
|
||||
originalStderr := r.Stderr
|
||||
defer func() {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
r.Stdout = originalStdout
|
||||
r.Stderr = originalStderr
|
||||
}()
|
||||
@@ -141,8 +156,8 @@ OutputLoop:
|
||||
// should be called by communicators who are running a remote command in
|
||||
// order to set that the command is done.
|
||||
func (r *RemoteCmd) SetExited(status int) {
|
||||
r.l.Lock()
|
||||
defer r.l.Unlock()
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if r.exitCh == nil {
|
||||
r.exitCh = make(chan struct{})
|
||||
@@ -156,11 +171,11 @@ func (r *RemoteCmd) SetExited(status int) {
|
||||
// Wait waits for the remote command to complete.
|
||||
func (r *RemoteCmd) Wait() {
|
||||
// Make sure our condition variable is initialized.
|
||||
r.l.Lock()
|
||||
r.Lock()
|
||||
if r.exitCh == nil {
|
||||
r.exitCh = make(chan struct{})
|
||||
}
|
||||
r.l.Unlock()
|
||||
r.Unlock()
|
||||
|
||||
<-r.exitCh
|
||||
}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// MockCommunicator is a valid Communicator implementation that can be
|
||||
// used for tests.
|
||||
type MockCommunicator struct {
|
||||
StartCalled bool
|
||||
StartCmd *RemoteCmd
|
||||
StartStderr string
|
||||
StartStdout string
|
||||
StartStdin string
|
||||
StartExitStatus int
|
||||
|
||||
UploadCalled bool
|
||||
UploadPath string
|
||||
UploadData string
|
||||
|
||||
UploadDirDst string
|
||||
UploadDirSrc string
|
||||
UploadDirExclude []string
|
||||
|
||||
DownloadCalled bool
|
||||
DownloadPath string
|
||||
DownloadData string
|
||||
}
|
||||
|
||||
func (c *MockCommunicator) Start(rc *RemoteCmd) error {
|
||||
c.StartCalled = true
|
||||
c.StartCmd = rc
|
||||
|
||||
go func() {
|
||||
var wg sync.WaitGroup
|
||||
if rc.Stdout != nil && c.StartStdout != "" {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
rc.Stdout.Write([]byte(c.StartStdout))
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if rc.Stderr != nil && c.StartStderr != "" {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
rc.Stderr.Write([]byte(c.StartStderr))
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
|
||||
if rc.Stdin != nil {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
var data bytes.Buffer
|
||||
io.Copy(&data, rc.Stdin)
|
||||
c.StartStdin = data.String()
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
rc.SetExited(c.StartExitStatus)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MockCommunicator) Upload(path string, r io.Reader) error {
|
||||
c.UploadCalled = true
|
||||
c.UploadPath = path
|
||||
|
||||
var data bytes.Buffer
|
||||
if _, err := io.Copy(&data, r); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
c.UploadData = data.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MockCommunicator) UploadDir(dst string, src string, excl []string) error {
|
||||
c.UploadDirDst = dst
|
||||
c.UploadDirSrc = src
|
||||
c.UploadDirExclude = excl
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *MockCommunicator) Download(path string, w io.Writer) error {
|
||||
c.DownloadCalled = true
|
||||
c.DownloadPath = path
|
||||
w.Write([]byte(c.DownloadData))
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMockCommunicator_impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(MockCommunicator)
|
||||
if _, ok := raw.(Communicator); !ok {
|
||||
t.Fatal("should be a communicator")
|
||||
}
|
||||
}
|
||||
@@ -2,51 +2,19 @@ package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type TestCommunicator struct {
|
||||
Stderr io.Reader
|
||||
Stdout io.Reader
|
||||
}
|
||||
|
||||
func (c *TestCommunicator) Start(rc *RemoteCmd) error {
|
||||
go func() {
|
||||
if rc.Stdout != nil && c.Stdout != nil {
|
||||
io.Copy(rc.Stdout, c.Stdout)
|
||||
}
|
||||
|
||||
if rc.Stderr != nil && c.Stderr != nil {
|
||||
io.Copy(rc.Stderr, c.Stderr)
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TestCommunicator) Upload(string, io.Reader) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *TestCommunicator) Download(string, io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestRemoteCmd_StartWithUi(t *testing.T) {
|
||||
data := "hello\nworld\nthere"
|
||||
|
||||
originalOutput := new(bytes.Buffer)
|
||||
rcOutput := new(bytes.Buffer)
|
||||
uiOutput := new(bytes.Buffer)
|
||||
rcOutput.WriteString(data)
|
||||
|
||||
testComm := &TestCommunicator{
|
||||
Stdout: rcOutput,
|
||||
}
|
||||
|
||||
testComm := new(MockCommunicator)
|
||||
testComm.StartStdout = data
|
||||
testUi := &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: uiOutput,
|
||||
@@ -57,22 +25,20 @@ func TestRemoteCmd_StartWithUi(t *testing.T) {
|
||||
Stdout: originalOutput,
|
||||
}
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
rc.SetExited(0)
|
||||
}()
|
||||
|
||||
err := rc.StartWithUi(testComm, testUi)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if uiOutput.String() != strings.TrimSpace(data)+"\n" {
|
||||
rc.Wait()
|
||||
|
||||
expected := strings.TrimSpace(data)
|
||||
if uiOutput.String() != expected+"\n" {
|
||||
t.Fatalf("bad output: '%s'", uiOutput.String())
|
||||
}
|
||||
|
||||
if originalOutput.String() != data {
|
||||
t.Fatalf("original is bad: '%s'", originalOutput.String())
|
||||
if originalOutput.String() != expected {
|
||||
t.Fatalf("bad: %#v", originalOutput.String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@@ -8,19 +8,19 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Template processes string data as a text/template with some common
|
||||
// ConfigTemplate processes string data as a text/template with some common
|
||||
// elements and functions available. Plugin creators should process as
|
||||
// many fields as possible through this.
|
||||
type Template struct {
|
||||
type ConfigTemplate struct {
|
||||
UserVars map[string]string
|
||||
|
||||
root *template.Template
|
||||
i int
|
||||
}
|
||||
|
||||
// NewTemplate creates a new template processor.
|
||||
func NewTemplate() (*Template, error) {
|
||||
result := &Template{
|
||||
// NewConfigTemplate creates a new configuration template processor.
|
||||
func NewConfigTemplate() (*ConfigTemplate, error) {
|
||||
result := &ConfigTemplate{
|
||||
UserVars: make(map[string]string),
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func NewTemplate() (*Template, error) {
|
||||
}
|
||||
|
||||
// Process processes a single string, compiling and executing the template.
|
||||
func (t *Template) Process(s string, data interface{}) (string, error) {
|
||||
func (t *ConfigTemplate) Process(s string, data interface{}) (string, error) {
|
||||
tpl, err := t.root.New(t.nextTemplateName()).Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
@@ -49,7 +49,7 @@ func (t *Template) Process(s string, data interface{}) (string, error) {
|
||||
}
|
||||
|
||||
// Validate the template.
|
||||
func (t *Template) Validate(s string) error {
|
||||
func (t *ConfigTemplate) Validate(s string) error {
|
||||
root, err := t.root.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -59,7 +59,7 @@ func (t *Template) Validate(s string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *Template) nextTemplateName() string {
|
||||
func (t *ConfigTemplate) nextTemplateName() string {
|
||||
name := fmt.Sprintf("tpl%d", t.i)
|
||||
t.i++
|
||||
return name
|
||||
@@ -67,7 +67,7 @@ func (t *Template) nextTemplateName() string {
|
||||
|
||||
// User is the function exposed as "user" within the templates and
|
||||
// looks up user variables.
|
||||
func (t *Template) templateUser(n string) (string, error) {
|
||||
func (t *ConfigTemplate) templateUser(n string) (string, error) {
|
||||
result, ok := t.UserVars[n]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("uknown user var: %s", n)
|
||||
@@ -1,4 +1,4 @@
|
||||
package common
|
||||
package packer
|
||||
|
||||
import (
|
||||
"math"
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTemplateProcess_timestamp(t *testing.T) {
|
||||
tpl, err := NewTemplate()
|
||||
func TestConfigTemplateProcess_timestamp(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
@@ -29,8 +29,8 @@ func TestTemplateProcess_timestamp(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateProcess_user(t *testing.T) {
|
||||
tpl, err := NewTemplate()
|
||||
func TestConfigTemplateProcess_user(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
@@ -47,8 +47,8 @@ func TestTemplateProcess_user(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateValidate(t *testing.T) {
|
||||
tpl, err := NewTemplate()
|
||||
func TestConfigTemplateValidate(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
+65
-21
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// The function type used to lookup Builder implementations.
|
||||
@@ -68,6 +69,12 @@ type EnvironmentConfig struct {
|
||||
Ui Ui
|
||||
}
|
||||
|
||||
type helpCommandEntry struct {
|
||||
i int
|
||||
key string
|
||||
synopsis string
|
||||
}
|
||||
|
||||
// DefaultEnvironmentConfig returns a default EnvironmentConfig that can
|
||||
// be used to create a new enviroment with NewEnvironment with sane defaults.
|
||||
func DefaultEnvironmentConfig() *EnvironmentConfig {
|
||||
@@ -245,7 +252,7 @@ func (e *coreEnvironment) Cli(args []string) (result int, err error) {
|
||||
|
||||
// If we still don't have a command, show the help.
|
||||
if command == nil {
|
||||
log.Printf("Environment.CLI: command not found: %s\n", args[0])
|
||||
e.ui.Error(fmt.Sprintf("Unknown command: %s\n", args[0]))
|
||||
e.printHelp()
|
||||
return 1, nil
|
||||
}
|
||||
@@ -266,42 +273,79 @@ func (e *coreEnvironment) Cli(args []string) (result int, err error) {
|
||||
func (e *coreEnvironment) printHelp() {
|
||||
// Created a sorted slice of the map keys and record the longest
|
||||
// command name so we can better format the output later.
|
||||
i := 0
|
||||
maxKeyLen := 0
|
||||
for _, command := range e.commands {
|
||||
if len(command) > maxKeyLen {
|
||||
maxKeyLen = len(command)
|
||||
}
|
||||
|
||||
i++
|
||||
}
|
||||
|
||||
// Sort the keys
|
||||
sort.Strings(e.commands)
|
||||
|
||||
// Create the communication/sync mechanisms to get the synopsis' of
|
||||
// the various commands. We do this in parallel since the overhead
|
||||
// of the subprocess underneath is very expensive and this speeds things
|
||||
// up an incredible amount.
|
||||
var wg sync.WaitGroup
|
||||
ch := make(chan *helpCommandEntry)
|
||||
|
||||
for i, key := range e.commands {
|
||||
wg.Add(1)
|
||||
|
||||
// Get the synopsis in a goroutine since it may take awhile
|
||||
// to subprocess out.
|
||||
go func(i int, key string) {
|
||||
defer wg.Done()
|
||||
var synopsis string
|
||||
command, err := e.components.Command(key)
|
||||
if err != nil {
|
||||
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
|
||||
} else if command == nil {
|
||||
return
|
||||
} else {
|
||||
synopsis = command.Synopsis()
|
||||
}
|
||||
|
||||
// Pad the key with spaces so that they're all the same width
|
||||
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||
|
||||
// Output the command and the synopsis
|
||||
ch <- &helpCommandEntry{
|
||||
i: i,
|
||||
key: key,
|
||||
synopsis: synopsis,
|
||||
}
|
||||
}(i, key)
|
||||
}
|
||||
|
||||
e.ui.Say("usage: packer [--version] [--help] <command> [<args>]\n")
|
||||
e.ui.Say("Available commands are:")
|
||||
for _, key := range e.commands {
|
||||
var synopsis string
|
||||
|
||||
command, err := e.components.Command(key)
|
||||
if err != nil {
|
||||
synopsis = fmt.Sprintf("Error loading command: %s", err.Error())
|
||||
} else if command == nil {
|
||||
continue
|
||||
} else {
|
||||
synopsis = command.Synopsis()
|
||||
// Make a goroutine that just waits for all the synopsis gathering
|
||||
// to complete, and then output it.
|
||||
synopsisDone := make(chan struct{})
|
||||
go func() {
|
||||
defer close(synopsisDone)
|
||||
entries := make([]string, len(e.commands))
|
||||
|
||||
for entry := range ch {
|
||||
e.ui.Machine("command", entry.key, entry.synopsis)
|
||||
message := fmt.Sprintf(" %s %s", entry.key, entry.synopsis)
|
||||
entries[entry.i] = message
|
||||
}
|
||||
|
||||
// Machine-readable output of the available command
|
||||
e.ui.Machine("command", key, synopsis)
|
||||
for _, message := range entries {
|
||||
if message != "" {
|
||||
e.ui.Say(message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Pad the key with spaces so that they're all the same width
|
||||
key = fmt.Sprintf("%v%v", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||
|
||||
// Output the command and the synopsis
|
||||
e.ui.Say(fmt.Sprintf(" %v %v", key, synopsis))
|
||||
}
|
||||
// Wait to complete getting the synopsis' then close the channel
|
||||
wg.Wait()
|
||||
close(ch)
|
||||
<-synopsisDone
|
||||
|
||||
e.ui.Say("\nGlobally recognized options:")
|
||||
e.ui.Say(" -machine-readable Machine-readable output format.")
|
||||
|
||||
@@ -17,6 +17,17 @@ func init() {
|
||||
log.SetOutput(ioutil.Discard)
|
||||
}
|
||||
|
||||
func testComponentFinder() *ComponentFinder {
|
||||
builderFactory := func(n string) (Builder, error) { return testBuilder(), nil }
|
||||
ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil }
|
||||
provFactory := func(n string) (Provisioner, error) { return new(TestProvisioner), nil }
|
||||
return &ComponentFinder{
|
||||
Builder: builderFactory,
|
||||
PostProcessor: ppFactory,
|
||||
Provisioner: provFactory,
|
||||
}
|
||||
}
|
||||
|
||||
func testEnvironment() Environment {
|
||||
config := DefaultEnvironmentConfig()
|
||||
config.Ui = &BasicUi{
|
||||
|
||||
@@ -37,10 +37,6 @@ func MultiErrorAppend(err error, errs ...error) *MultiError {
|
||||
err = new(MultiError)
|
||||
}
|
||||
|
||||
if err.Errors == nil {
|
||||
err.Errors = make([]error, 0, len(errs))
|
||||
}
|
||||
|
||||
err.Errors = append(err.Errors, errs...)
|
||||
return err
|
||||
default:
|
||||
|
||||
@@ -40,7 +40,7 @@ func (b *cmdBuilder) Cancel() {
|
||||
func (c *cmdBuilder) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() && cb != nil {
|
||||
cb()
|
||||
} else if p != nil {
|
||||
} else if p != nil && !Killed {
|
||||
log.Panic(p)
|
||||
}
|
||||
}
|
||||
|
||||
+65
-36
@@ -2,7 +2,6 @@ package plugin
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
@@ -10,6 +9,7 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net"
|
||||
"net/rpc"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -19,6 +19,10 @@ import (
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// If this is true, then the "unexpected EOF" panic will not be
|
||||
// raised throughout the clients.
|
||||
var Killed = false
|
||||
|
||||
// This is a slice of the "managed" clients which are cleaned up when
|
||||
// calling Cleanup
|
||||
var managedClients = make([]*Client, 0, 5)
|
||||
@@ -68,6 +72,9 @@ type ClientConfig struct {
|
||||
//
|
||||
// This must only be called _once_.
|
||||
func CleanupClients() {
|
||||
// Set the killed to true so that we don't get unexpected panics
|
||||
Killed = true
|
||||
|
||||
// Kill all the managed clients in parallel and use a WaitGroup
|
||||
// to wait for them all to finish up.
|
||||
var wg sync.WaitGroup
|
||||
@@ -115,6 +122,8 @@ func NewClient(config *ClientConfig) (c *Client) {
|
||||
|
||||
// Tells whether or not the underlying process has exited.
|
||||
func (c *Client) Exited() bool {
|
||||
c.l.Lock()
|
||||
defer c.l.Unlock()
|
||||
return c.exited
|
||||
}
|
||||
|
||||
@@ -214,7 +223,7 @@ func (c *Client) Start() (address string, err error) {
|
||||
fmt.Sprintf("PACKER_PLUGIN_MAX_PORT=%d", c.config.MaxPort),
|
||||
}
|
||||
|
||||
stdout := new(bytes.Buffer)
|
||||
stdout_r, stdout_w := io.Pipe()
|
||||
stderr_r, stderr_w := io.Pipe()
|
||||
|
||||
cmd := c.config.Cmd
|
||||
@@ -222,7 +231,7 @@ func (c *Client) Start() (address string, err error) {
|
||||
cmd.Env = append(cmd.Env, env...)
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = stderr_w
|
||||
cmd.Stdout = stdout
|
||||
cmd.Stdout = stdout_w
|
||||
|
||||
log.Printf("Starting plugin: %s %#v", cmd.Path, cmd.Args)
|
||||
err = cmd.Start()
|
||||
@@ -244,10 +253,12 @@ func (c *Client) Start() (address string, err error) {
|
||||
}()
|
||||
|
||||
// Start goroutine to wait for process to exit
|
||||
exitCh := make(chan struct{})
|
||||
go func() {
|
||||
// Make sure we close the write end of our stderr listener so
|
||||
// that the log goroutine ends properly.
|
||||
// Make sure we close the write end of our stderr/stdout so
|
||||
// that the readers send EOF properly.
|
||||
defer stderr_w.Close()
|
||||
defer stdout_w.Close()
|
||||
|
||||
// Wait for the command to end.
|
||||
cmd.Wait()
|
||||
@@ -257,46 +268,60 @@ func (c *Client) Start() (address string, err error) {
|
||||
os.Stderr.Sync()
|
||||
|
||||
// Mark that we exited
|
||||
close(exitCh)
|
||||
|
||||
// Set that we exited, which takes a lock
|
||||
c.l.Lock()
|
||||
defer c.l.Unlock()
|
||||
c.exited = true
|
||||
}()
|
||||
|
||||
// Start goroutine that logs the stderr
|
||||
go c.logStderr(stderr_r)
|
||||
|
||||
// Start a goroutine that is going to be reading the lines
|
||||
// out of stdout
|
||||
linesCh := make(chan []byte)
|
||||
go func() {
|
||||
defer close(linesCh)
|
||||
|
||||
buf := bufio.NewReader(stdout_r)
|
||||
for {
|
||||
line, err := buf.ReadBytes('\n')
|
||||
if line != nil {
|
||||
linesCh <- line
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Make sure after we exit we read the lines from stdout forever
|
||||
// so they dont' block since it is an io.Pipe
|
||||
defer func() {
|
||||
go func() {
|
||||
for _ = range linesCh {
|
||||
}
|
||||
}()
|
||||
}()
|
||||
|
||||
// Some channels for the next step
|
||||
timeout := time.After(c.config.StartTimeout)
|
||||
|
||||
// Start looking for the address
|
||||
log.Printf("Waiting for RPC address for: %s", cmd.Path)
|
||||
for done := false; !done; {
|
||||
select {
|
||||
case <-timeout:
|
||||
err = errors.New("timeout while waiting for plugin to start")
|
||||
done = true
|
||||
default:
|
||||
}
|
||||
|
||||
if err == nil && c.Exited() {
|
||||
err = errors.New("plugin exited before we could connect")
|
||||
done = true
|
||||
}
|
||||
|
||||
if line, lerr := stdout.ReadBytes('\n'); lerr == nil {
|
||||
// Trim the address and reset the err since we were able
|
||||
// to read some sort of address.
|
||||
c.address = strings.TrimSpace(string(line))
|
||||
address = c.address
|
||||
err = nil
|
||||
break
|
||||
}
|
||||
|
||||
// If error is nil from previously, return now
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Wait a bit
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
select {
|
||||
case <-timeout:
|
||||
err = errors.New("timeout while waiting for plugin to start")
|
||||
case <-exitCh:
|
||||
err = errors.New("plugin exited before we could connect")
|
||||
case line := <-linesCh:
|
||||
// Trim the address and reset the err since we were able
|
||||
// to read some sort of address.
|
||||
c.address = strings.TrimSpace(string(line))
|
||||
address = c.address
|
||||
}
|
||||
|
||||
return
|
||||
@@ -328,10 +353,14 @@ func (c *Client) rpcClient() (*rpc.Client, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client, err := rpc.Dial("tcp", address)
|
||||
conn, err := net.Dial("tcp", address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return client, nil
|
||||
// Make sure to set keep alive so that the connection doesn't die
|
||||
tcpConn := conn.(*net.TCPConn)
|
||||
tcpConn.SetKeepAlive(true)
|
||||
|
||||
return rpc.NewClient(tcpConn), nil
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (c *cmdCommand) Synopsis() (result string) {
|
||||
func (c *cmdCommand) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() {
|
||||
cb()
|
||||
} else if p != nil {
|
||||
} else if p != nil && !Killed {
|
||||
log.Panic(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ func (c *cmdHook) Run(name string, ui packer.Ui, comm packer.Communicator, data
|
||||
func (c *cmdHook) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() {
|
||||
cb()
|
||||
} else if p != nil {
|
||||
} else if p != nil && !Killed {
|
||||
log.Panic(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +94,10 @@ func swallowInterrupts() {
|
||||
signal.Notify(ch, os.Interrupt)
|
||||
|
||||
go func() {
|
||||
<-ch
|
||||
log.Println("Received interrupt signal. Ignoring.")
|
||||
for {
|
||||
<-ch
|
||||
log.Println("Received interrupt signal. Ignoring.")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *cmdPostProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.
|
||||
func (c *cmdPostProcessor) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() {
|
||||
cb()
|
||||
} else if p != nil {
|
||||
} else if p != nil && !Killed {
|
||||
log.Panic(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func (c *cmdProvisioner) Provision(ui packer.Ui, comm packer.Communicator) error
|
||||
func (c *cmdProvisioner) checkExit(p interface{}, cb func()) {
|
||||
if c.client.Exited() {
|
||||
cb()
|
||||
} else if p != nil {
|
||||
} else if p != nil && !Killed {
|
||||
log.Panic(p)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package rpc
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net"
|
||||
@@ -60,20 +61,20 @@ func (b *builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
go func() {
|
||||
defer responseL.Close()
|
||||
|
||||
var response BuilderRunResponse
|
||||
defer func() { runResponseCh <- &response }()
|
||||
|
||||
conn, err := responseL.Accept()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
response.Err = err
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
decoder := gob.NewDecoder(conn)
|
||||
|
||||
var response BuilderRunResponse
|
||||
if err := decoder.Decode(&response); err != nil {
|
||||
log.Panic(err)
|
||||
response.Err = fmt.Errorf("Error waiting for Run: %s", err)
|
||||
}
|
||||
|
||||
runResponseCh <- &response
|
||||
}()
|
||||
|
||||
args := &BuilderRunArgs{
|
||||
@@ -104,7 +105,7 @@ func (b *builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
|
||||
func (b *builder) Cancel() {
|
||||
if err := b.client.Call("Builder.Cancel", new(interface{}), new(interface{})); err != nil {
|
||||
panic(err)
|
||||
log.Printf("Error cancelling builder: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,7 +154,7 @@ func (b *BuilderServer) Run(args *BuilderRunArgs, reply *interface{}) error {
|
||||
|
||||
err := responseWriter.Encode(&BuilderRunResponse{responseErr, responseAddress})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
log.Printf("BuildServer.Run error: %s", err)
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
@@ -44,6 +44,12 @@ type CommunicatorUploadArgs struct {
|
||||
ReaderAddress string
|
||||
}
|
||||
|
||||
type CommunicatorUploadDirArgs struct {
|
||||
Dst string
|
||||
Src string
|
||||
Exclude []string
|
||||
}
|
||||
|
||||
func Communicator(client *rpc.Client) *communicator {
|
||||
return &communicator{client}
|
||||
}
|
||||
@@ -78,7 +84,8 @@ func (c *communicator) Start(cmd *packer.RemoteCmd) (err error) {
|
||||
|
||||
conn, err := responseL.Accept()
|
||||
if err != nil {
|
||||
log.Panic(err)
|
||||
cmd.SetExited(123)
|
||||
return
|
||||
}
|
||||
|
||||
defer conn.Close()
|
||||
@@ -87,7 +94,8 @@ func (c *communicator) Start(cmd *packer.RemoteCmd) (err error) {
|
||||
|
||||
var finished CommandFinished
|
||||
if err := decoder.Decode(&finished); err != nil {
|
||||
log.Panic(err)
|
||||
cmd.SetExited(123)
|
||||
return
|
||||
}
|
||||
|
||||
cmd.SetExited(finished.ExitStatus)
|
||||
@@ -121,6 +129,22 @@ func (c *communicator) Upload(path string, r io.Reader) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||
args := &CommunicatorUploadDirArgs{
|
||||
Dst: dst,
|
||||
Src: src,
|
||||
Exclude: exclude,
|
||||
}
|
||||
|
||||
var reply error
|
||||
err := c.client.Call("Communicator.UploadDir", args, &reply)
|
||||
if err == nil {
|
||||
err = reply
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *communicator) Download(path string, w io.Writer) (err error) {
|
||||
// We need to create a server that can proxy that data downloaded
|
||||
// into the writer because we can't gob encode a writer directly.
|
||||
@@ -221,6 +245,10 @@ func (c *CommunicatorServer) Upload(args *CommunicatorUploadArgs, reply *interfa
|
||||
return
|
||||
}
|
||||
|
||||
func (c *CommunicatorServer) UploadDir(args *CommunicatorUploadDirArgs, reply *error) error {
|
||||
return c.c.UploadDir(args.Dst, args.Src, args.Exclude)
|
||||
}
|
||||
|
||||
func (c *CommunicatorServer) Download(args *CommunicatorDownloadArgs, reply *interface{}) (err error) {
|
||||
writerC, err := net.Dial("tcp", args.WriterAddress)
|
||||
if err != nil {
|
||||
|
||||
+110
-89
@@ -2,52 +2,16 @@ package rpc
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"cgl.tideland.biz/asserts"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"net/rpc"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type testCommunicator struct {
|
||||
startCalled bool
|
||||
startCmd *packer.RemoteCmd
|
||||
|
||||
uploadCalled bool
|
||||
uploadPath string
|
||||
uploadData string
|
||||
|
||||
downloadCalled bool
|
||||
downloadPath string
|
||||
}
|
||||
|
||||
func (t *testCommunicator) Start(cmd *packer.RemoteCmd) error {
|
||||
t.startCalled = true
|
||||
t.startCmd = cmd
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *testCommunicator) Upload(path string, reader io.Reader) (err error) {
|
||||
t.uploadCalled = true
|
||||
t.uploadPath = path
|
||||
t.uploadData, err = bufio.NewReader(reader).ReadString('\n')
|
||||
return
|
||||
}
|
||||
|
||||
func (t *testCommunicator) Download(path string, writer io.Writer) error {
|
||||
t.downloadCalled = true
|
||||
t.downloadPath = path
|
||||
writer.Write([]byte("download\n"))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCommunicatorRPC(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
// Create the interface to test
|
||||
c := new(testCommunicator)
|
||||
c := new(packer.MockCommunicator)
|
||||
|
||||
// Start the server
|
||||
server := rpc.NewServer()
|
||||
@@ -56,7 +20,9 @@ func TestCommunicatorRPC(t *testing.T) {
|
||||
|
||||
// Create the client over RPC and run some methods to verify it works
|
||||
client, err := rpc.Dial("tcp", address)
|
||||
assert.Nil(err, "should be able to connect")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
remote := Communicator(client)
|
||||
|
||||
// The remote command we'll use
|
||||
@@ -70,53 +36,95 @@ func TestCommunicatorRPC(t *testing.T) {
|
||||
cmd.Stdout = stdout_w
|
||||
cmd.Stderr = stderr_w
|
||||
|
||||
// Send some data on stdout and stderr from the mock
|
||||
c.StartStdout = "outfoo\n"
|
||||
c.StartStderr = "errfoo\n"
|
||||
c.StartExitStatus = 42
|
||||
|
||||
// Test Start
|
||||
err = remote.Start(&cmd)
|
||||
assert.Nil(err, "should not have an error")
|
||||
|
||||
// Test that we can read from stdout
|
||||
c.startCmd.Stdout.Write([]byte("outfoo\n"))
|
||||
bufOut := bufio.NewReader(stdout_r)
|
||||
data, err := bufOut.ReadString('\n')
|
||||
assert.Nil(err, "should have no problem reading stdout")
|
||||
assert.Equal(data, "outfoo\n", "should be correct stdout")
|
||||
|
||||
// Test that we can read from stderr
|
||||
c.startCmd.Stderr.Write([]byte("errfoo\n"))
|
||||
bufErr := bufio.NewReader(stderr_r)
|
||||
data, err = bufErr.ReadString('\n')
|
||||
assert.Nil(err, "should have no problem reading stderr")
|
||||
assert.Equal(data, "errfoo\n", "should be correct stderr")
|
||||
|
||||
// Test that we can write to stdin
|
||||
stdin_w.Write([]byte("infoo\n"))
|
||||
bufIn := bufio.NewReader(c.startCmd.Stdin)
|
||||
data, err = bufIn.ReadString('\n')
|
||||
assert.Nil(err, "should have no problem reading stdin")
|
||||
assert.Equal(data, "infoo\n", "should be correct stdin")
|
||||
|
||||
// Test that we can get the exit status properly
|
||||
c.startCmd.SetExited(42)
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
if cmd.Exited {
|
||||
assert.Equal(cmd.ExitStatus, 42, "should have proper exit status")
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
assert.True(cmd.Exited, "should have exited")
|
||||
// Test that we can read from stdout
|
||||
bufOut := bufio.NewReader(stdout_r)
|
||||
data, err := bufOut.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if data != "outfoo\n" {
|
||||
t.Fatalf("bad data: %s", data)
|
||||
}
|
||||
|
||||
// Test that we can read from stderr
|
||||
bufErr := bufio.NewReader(stderr_r)
|
||||
data, err = bufErr.ReadString('\n')
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if data != "errfoo\n" {
|
||||
t.Fatalf("bad data: %s", data)
|
||||
}
|
||||
|
||||
// Test that we can write to stdin
|
||||
stdin_w.Write([]byte("info\n"))
|
||||
stdin_w.Close()
|
||||
cmd.Wait()
|
||||
if c.StartStdin != "info\n" {
|
||||
t.Fatalf("bad data: %s", data)
|
||||
}
|
||||
|
||||
// Test that we can get the exit status properly
|
||||
if cmd.ExitStatus != 42 {
|
||||
t.Fatalf("bad exit: %d", cmd.ExitStatus)
|
||||
}
|
||||
|
||||
// Test that we can upload things
|
||||
uploadR, uploadW := io.Pipe()
|
||||
go uploadW.Write([]byte("uploadfoo\n"))
|
||||
go func() {
|
||||
defer uploadW.Close()
|
||||
uploadW.Write([]byte("uploadfoo\n"))
|
||||
}()
|
||||
err = remote.Upload("foo", uploadR)
|
||||
assert.Nil(err, "should not error")
|
||||
assert.True(c.uploadCalled, "should be called")
|
||||
assert.Equal(c.uploadPath, "foo", "should be correct path")
|
||||
assert.Equal(c.uploadData, "uploadfoo\n", "should have the proper data")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !c.UploadCalled {
|
||||
t.Fatal("should have uploaded")
|
||||
}
|
||||
|
||||
if c.UploadPath != "foo" {
|
||||
t.Fatalf("path: %s", c.UploadPath)
|
||||
}
|
||||
|
||||
if c.UploadData != "uploadfoo\n" {
|
||||
t.Fatalf("bad: %s", c.UploadData)
|
||||
}
|
||||
|
||||
// Test that we can upload directories
|
||||
dirDst := "foo"
|
||||
dirSrc := "bar"
|
||||
dirExcl := []string{"foo"}
|
||||
err = remote.UploadDir(dirDst, dirSrc, dirExcl)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if c.UploadDirDst != dirDst {
|
||||
t.Fatalf("bad: %s", c.UploadDirDst)
|
||||
}
|
||||
|
||||
if c.UploadDirSrc != dirSrc {
|
||||
t.Fatalf("bad: %s", c.UploadDirSrc)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(c.UploadDirExclude, dirExcl) {
|
||||
t.Fatalf("bad: %#v", c.UploadDirExclude)
|
||||
}
|
||||
|
||||
// Test that we can download things
|
||||
downloadR, downloadW := io.Pipe()
|
||||
@@ -130,21 +138,34 @@ func TestCommunicatorRPC(t *testing.T) {
|
||||
downloadDone <- true
|
||||
}()
|
||||
|
||||
c.DownloadData = "download\n"
|
||||
err = remote.Download("bar", downloadW)
|
||||
assert.Nil(err, "should not error")
|
||||
assert.True(c.downloadCalled, "should have called download")
|
||||
assert.Equal(c.downloadPath, "bar", "should have correct download path")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !c.DownloadCalled {
|
||||
t.Fatal("download should be called")
|
||||
}
|
||||
|
||||
if c.DownloadPath != "bar" {
|
||||
t.Fatalf("bad: %s", c.DownloadPath)
|
||||
}
|
||||
|
||||
<-downloadDone
|
||||
assert.Nil(downloadErr, "should not error reading download data")
|
||||
assert.Equal(downloadData, "download\n", "should have the proper data")
|
||||
if downloadErr != nil {
|
||||
t.Fatalf("err: %s", downloadErr)
|
||||
}
|
||||
|
||||
if downloadData != "download\n" {
|
||||
t.Fatalf("bad: %s", downloadData)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCommunicator_ImplementsCommunicator(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
var r packer.Communicator
|
||||
c := Communicator(nil)
|
||||
|
||||
assert.Implementor(c, &r, "should be a Communicator")
|
||||
var raw interface{}
|
||||
raw = Communicator(nil)
|
||||
if _, ok := raw.(packer.Communicator); !ok {
|
||||
t.Fatal("should be a Communicator")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ func TestProvisionerRPC(t *testing.T) {
|
||||
|
||||
// Test Provision
|
||||
ui := &testUi{}
|
||||
comm := &testCommunicator{}
|
||||
comm := new(packer.MockCommunicator)
|
||||
pClient.Provision(ui, comm)
|
||||
assert.True(p.provCalled, "provision should be called")
|
||||
|
||||
|
||||
+5
-4
@@ -2,6 +2,7 @@ package rpc
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"net/rpc"
|
||||
)
|
||||
|
||||
@@ -30,7 +31,7 @@ func (u *Ui) Ask(query string) (result string, err error) {
|
||||
|
||||
func (u *Ui) Error(message string) {
|
||||
if err := u.client.Call("Ui.Error", message, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
log.Printf("Error in Ui RPC call: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,19 +42,19 @@ func (u *Ui) Machine(t string, args ...string) {
|
||||
}
|
||||
|
||||
if err := u.client.Call("Ui.Machine", rpcArgs, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
log.Printf("Error in Ui RPC call: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ui) Message(message string) {
|
||||
if err := u.client.Call("Ui.Message", message, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
log.Printf("Error in Ui RPC call: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ui) Say(message string) {
|
||||
if err := u.client.Call("Ui.Say", message, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
log.Printf("Error in Ui RPC call: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+56
-28
@@ -1,10 +1,13 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
jsonutil "github.com/mitchellh/packer/common/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
@@ -24,40 +27,40 @@ type rawTemplate struct {
|
||||
// completed form it can be without additional processing by the caller.
|
||||
type Template struct {
|
||||
Variables map[string]string
|
||||
Builders map[string]rawBuilderConfig
|
||||
Builders map[string]RawBuilderConfig
|
||||
Hooks map[string][]string
|
||||
PostProcessors [][]rawPostProcessorConfig
|
||||
Provisioners []rawProvisionerConfig
|
||||
PostProcessors [][]RawPostProcessorConfig
|
||||
Provisioners []RawProvisionerConfig
|
||||
}
|
||||
|
||||
// The rawBuilderConfig struct represents a raw, unprocessed builder
|
||||
// The RawBuilderConfig struct represents a raw, unprocessed builder
|
||||
// configuration. It contains the name of the builder as well as the
|
||||
// raw configuration. If requested, this is used to compile into a full
|
||||
// builder configuration at some point.
|
||||
type rawBuilderConfig struct {
|
||||
type RawBuilderConfig struct {
|
||||
Name string
|
||||
Type string
|
||||
|
||||
rawConfig interface{}
|
||||
RawConfig interface{}
|
||||
}
|
||||
|
||||
// rawPostProcessorConfig represents a raw, unprocessed post-processor
|
||||
// RawPostProcessorConfig represents a raw, unprocessed post-processor
|
||||
// configuration. It contains the type of the post processor as well as the
|
||||
// raw configuration that is handed to the post-processor for it to process.
|
||||
type rawPostProcessorConfig struct {
|
||||
type RawPostProcessorConfig struct {
|
||||
Type string
|
||||
KeepInputArtifact bool `mapstructure:"keep_input_artifact"`
|
||||
rawConfig interface{}
|
||||
RawConfig map[string]interface{}
|
||||
}
|
||||
|
||||
// rawProvisionerConfig represents a raw, unprocessed provisioner configuration.
|
||||
// RawProvisionerConfig represents a raw, unprocessed provisioner configuration.
|
||||
// It contains the type of the provisioner as well as the raw configuration
|
||||
// that is handed to the provisioner for it to process.
|
||||
type rawProvisionerConfig struct {
|
||||
type RawProvisionerConfig struct {
|
||||
Type string
|
||||
Override map[string]interface{}
|
||||
|
||||
rawConfig interface{}
|
||||
RawConfig interface{}
|
||||
}
|
||||
|
||||
// ParseTemplate takes a byte slice and parses a Template from it, returning
|
||||
@@ -103,10 +106,10 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
|
||||
t = &Template{}
|
||||
t.Variables = make(map[string]string)
|
||||
t.Builders = make(map[string]rawBuilderConfig)
|
||||
t.Builders = make(map[string]RawBuilderConfig)
|
||||
t.Hooks = rawTpl.Hooks
|
||||
t.PostProcessors = make([][]rawPostProcessorConfig, len(rawTpl.PostProcessors))
|
||||
t.Provisioners = make([]rawProvisionerConfig, len(rawTpl.Provisioners))
|
||||
t.PostProcessors = make([][]RawPostProcessorConfig, len(rawTpl.PostProcessors))
|
||||
t.Provisioners = make([]RawProvisionerConfig, len(rawTpl.Provisioners))
|
||||
|
||||
// Gather all the variables
|
||||
for k, v := range rawTpl.Variables {
|
||||
@@ -115,7 +118,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
|
||||
// Gather all the builders
|
||||
for i, v := range rawTpl.Builders {
|
||||
var raw rawBuilderConfig
|
||||
var raw RawBuilderConfig
|
||||
if err := mapstructure.Decode(v, &raw); err != nil {
|
||||
if merr, ok := err.(*mapstructure.Error); ok {
|
||||
for _, err := range merr.Errors {
|
||||
@@ -150,7 +153,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
// itself doesn't know about, and it will cause a validation error.
|
||||
delete(v, "name")
|
||||
|
||||
raw.rawConfig = v
|
||||
raw.RawConfig = v
|
||||
|
||||
t.Builders[raw.Name] = raw
|
||||
}
|
||||
@@ -159,13 +162,13 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
// are actually three different formats that the user can use to define
|
||||
// a post-processor.
|
||||
for i, rawV := range rawTpl.PostProcessors {
|
||||
rawPP, err := parsePostProvisioner(i, rawV)
|
||||
rawPP, err := parsePostProcessor(i, rawV)
|
||||
if err != nil {
|
||||
errors = append(errors, err...)
|
||||
continue
|
||||
}
|
||||
|
||||
t.PostProcessors[i] = make([]rawPostProcessorConfig, len(rawPP))
|
||||
t.PostProcessors[i] = make([]RawPostProcessorConfig, len(rawPP))
|
||||
configs := t.PostProcessors[i]
|
||||
for j, pp := range rawPP {
|
||||
config := &configs[j]
|
||||
@@ -186,7 +189,10 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
config.rawConfig = pp
|
||||
// Remove the input keep_input_artifact option
|
||||
delete(pp, "keep_input_artifact")
|
||||
|
||||
config.RawConfig = pp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +221,15 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
// actively reject them as invalid configuration.
|
||||
delete(v, "override")
|
||||
|
||||
raw.rawConfig = v
|
||||
// Verify that the override keys exist...
|
||||
for name, _ := range raw.Override {
|
||||
if _, ok := t.Builders[name]; !ok {
|
||||
errors = append(
|
||||
errors, fmt.Errorf("provisioner %d: build '%s' not found for override", i+1, name))
|
||||
}
|
||||
}
|
||||
|
||||
raw.RawConfig = v
|
||||
}
|
||||
|
||||
if len(t.Builders) == 0 {
|
||||
@@ -235,15 +249,29 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
// ParseTemplateFile takes the given template file and parses it into
|
||||
// a single template.
|
||||
func ParseTemplateFile(path string) (*Template, error) {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var data []byte
|
||||
|
||||
if path == "-" {
|
||||
// Read from stdin...
|
||||
buf := new(bytes.Buffer)
|
||||
_, err := io.Copy(buf, os.Stdin)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data = buf.Bytes()
|
||||
} else {
|
||||
var err error
|
||||
data, err = ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return ParseTemplate(data)
|
||||
}
|
||||
|
||||
func parsePostProvisioner(i int, rawV interface{}) (result []map[string]interface{}, errors []error) {
|
||||
func parsePostProcessor(i int, rawV interface{}) (result []map[string]interface{}, errors []error) {
|
||||
switch v := rawV.(type) {
|
||||
case string:
|
||||
result = []map[string]interface{}{
|
||||
@@ -365,7 +393,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
|
||||
current[i] = coreBuildPostProcessor{
|
||||
processor: pp,
|
||||
processorType: rawPP.Type,
|
||||
config: rawPP.rawConfig,
|
||||
config: rawPP.RawConfig,
|
||||
keepInputArtifact: rawPP.KeepInputArtifact,
|
||||
}
|
||||
}
|
||||
@@ -388,7 +416,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
|
||||
}
|
||||
|
||||
configs := make([]interface{}, 1, 2)
|
||||
configs[0] = rawProvisioner.rawConfig
|
||||
configs[0] = rawProvisioner.RawConfig
|
||||
|
||||
if rawProvisioner.Override != nil {
|
||||
if override, ok := rawProvisioner.Override[name]; ok {
|
||||
@@ -403,7 +431,7 @@ func (t *Template) Build(name string, components *ComponentFinder) (b Build, err
|
||||
b = &coreBuild{
|
||||
name: name,
|
||||
builder: builder,
|
||||
builderConfig: builderConfig.rawConfig,
|
||||
builderConfig: builderConfig.RawConfig,
|
||||
builderType: builderConfig.Type,
|
||||
hooks: hooks,
|
||||
postProcessors: postProcessors,
|
||||
|
||||
+76
-20
@@ -3,22 +3,12 @@ package packer
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testComponentFinder() *ComponentFinder {
|
||||
builderFactory := func(n string) (Builder, error) { return testBuilder(), nil }
|
||||
ppFactory := func(n string) (PostProcessor, error) { return new(TestPostProcessor), nil }
|
||||
provFactory := func(n string) (Provisioner, error) { return new(TestProvisioner), nil }
|
||||
return &ComponentFinder{
|
||||
Builder: builderFactory,
|
||||
PostProcessor: ppFactory,
|
||||
Provisioner: provFactory,
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplateFile_basic(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
@@ -43,6 +33,39 @@ func TestParseTemplateFile_basic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplateFile_stdin(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
"builders": [{"type": "something"}]
|
||||
}
|
||||
`
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
tf.Write([]byte(data))
|
||||
|
||||
// Sync and seek to the beginning so that we can re-read the contents
|
||||
tf.Sync()
|
||||
tf.Seek(0, 0)
|
||||
|
||||
// Set stdin to something we control
|
||||
oldStdin := os.Stdin
|
||||
defer func() { os.Stdin = oldStdin }()
|
||||
os.Stdin = tf
|
||||
|
||||
result, err := ParseTemplateFile("-")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if len(result.Builders) != 1 {
|
||||
t.Fatalf("bad: %#v", result.Builders)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTemplate_Basic(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -165,8 +188,8 @@ func TestParseTemplate_BuilderWithName(t *testing.T) {
|
||||
assert.True(ok, "should have bob builder")
|
||||
assert.Equal(builder.Type, "amazon-ebs", "builder should be amazon-ebs")
|
||||
|
||||
rawConfig := builder.rawConfig
|
||||
if rawConfig == nil {
|
||||
RawConfig := builder.RawConfig
|
||||
if RawConfig == nil {
|
||||
t.Fatal("missing builder raw config")
|
||||
}
|
||||
|
||||
@@ -174,8 +197,8 @@ func TestParseTemplate_BuilderWithName(t *testing.T) {
|
||||
"type": "amazon-ebs",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(rawConfig, expected) {
|
||||
t.Fatalf("bad raw: %#v", rawConfig)
|
||||
if !reflect.DeepEqual(RawConfig, expected) {
|
||||
t.Fatalf("bad raw: %#v", RawConfig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -333,7 +356,7 @@ func TestParseTemplate_Provisioners(t *testing.T) {
|
||||
assert.NotNil(result, "template should not be nil")
|
||||
assert.Length(result.Provisioners, 1, "should have one provisioner")
|
||||
assert.Equal(result.Provisioners[0].Type, "shell", "provisioner should be shell")
|
||||
assert.NotNil(result.Provisioners[0].rawConfig, "should have raw config")
|
||||
assert.NotNil(result.Provisioners[0].RawConfig, "should have raw config")
|
||||
}
|
||||
|
||||
func TestParseTemplate_Variables(t *testing.T) {
|
||||
@@ -600,6 +623,11 @@ func TestTemplate_Build(t *testing.T) {
|
||||
assert.Equal(len(coreBuild.postProcessors[1]), 2, "should have correct number")
|
||||
assert.False(coreBuild.postProcessors[1][0].keepInputArtifact, "shoule be correct")
|
||||
assert.True(coreBuild.postProcessors[1][1].keepInputArtifact, "shoule be correct")
|
||||
|
||||
config := coreBuild.postProcessors[1][1].config
|
||||
if _, ok := config["keep_input_artifact"]; ok {
|
||||
t.Fatal("should not have keep_input_artifact")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
|
||||
@@ -631,8 +659,8 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
rawConfig := template.Provisioners[0].rawConfig
|
||||
if rawConfig == nil {
|
||||
RawConfig := template.Provisioners[0].RawConfig
|
||||
if RawConfig == nil {
|
||||
t.Fatal("missing provisioner raw config")
|
||||
}
|
||||
|
||||
@@ -640,8 +668,8 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
|
||||
"type": "test-prov",
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(rawConfig, expected) {
|
||||
t.Fatalf("bad raw: %#v", rawConfig)
|
||||
if !reflect.DeepEqual(RawConfig, expected) {
|
||||
t.Fatalf("bad raw: %#v", RawConfig)
|
||||
}
|
||||
|
||||
builder := testBuilder()
|
||||
@@ -672,6 +700,34 @@ func TestTemplate_Build_ProvisionerOverride(t *testing.T) {
|
||||
assert.Equal(len(coreBuild.provisioners[0].config), 2, "should have two configs on the provisioner")
|
||||
}
|
||||
|
||||
func TestTemplate_Build_ProvisionerOverrideBad(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
"builders": [
|
||||
{
|
||||
"name": "test1",
|
||||
"type": "test-builder"
|
||||
}
|
||||
],
|
||||
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "test-prov",
|
||||
|
||||
"override": {
|
||||
"testNope": {}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
_, err := ParseTemplate([]byte(data))
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTemplateBuild_variables(t *testing.T) {
|
||||
data := `
|
||||
{
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user