Compare commits
218 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9907933bf8 | |||
| 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 | |||
| 0a39650332 | |||
| 1fd5937839 | |||
| 95435e484c | |||
| bc097abc72 | |||
| 856f27bc9b | |||
| 43c6b9e6d1 | |||
| 9a2dbd54bf | |||
| 1ec2de97a6 | |||
| 7d9c7530be | |||
| 049dc5bff5 | |||
| 724e2b3c52 | |||
| 0eb9e06637 | |||
| e00f114520 | |||
| c09c1108ca | |||
| 34556713dc | |||
| 4d7b0c2dbc | |||
| c1f8e5c3b3 | |||
| 97b4c7d22b | |||
| 4e300259c7 | |||
| db903af566 | |||
| 3c9e504475 | |||
| d88ff24343 | |||
| f7bd403151 | |||
| 1bf2bade7d | |||
| d4d58e3779 | |||
| 4f58b20479 | |||
| 758a53b146 | |||
| 336495d34c | |||
| 78d03c58e5 | |||
| 43b23c5221 | |||
| 226e6f5859 | |||
| bebed45a80 | |||
| f4a9d9d3b7 | |||
| eed6ece45b | |||
| 04c8592077 | |||
| f3bb028ec3 | |||
| f46bf47579 | |||
| ca54df1a76 | |||
| b54a5c0074 | |||
| cd62aa5003 | |||
| 10b0f920bc | |||
| f3f4ab91c7 | |||
| c8bc2bc1ba | |||
| 41370a215e | |||
| f2355a577a | |||
| 6a7450974d | |||
| 810bea7fcb | |||
| d5ab581260 | |||
| ae35cd51e2 | |||
| a4c555a5b7 | |||
| a542ff967b | |||
| 59eff6a222 | |||
| b25f0ceabf | |||
| a0a6817f21 | |||
| 5decc186a6 | |||
| cfc7715495 | |||
| 60fc565dbb | |||
| da4a7c3b70 | |||
| 2349706a68 | |||
| 777f595c8b | |||
| e7b1ba2f5e | |||
| 95926424f2 | |||
| 1e4e343333 | |||
| de13474865 | |||
| 3ca085c2c1 | |||
| 713791ba9a | |||
| bb1b3d8fe0 | |||
| 1a1b4ba715 | |||
| 41b3f9ebd9 | |||
| d0200b7be8 | |||
| a0033fba35 | |||
| 180d5c8275 | |||
| ddfd9be8cc | |||
| bf457f437c | |||
| 100458955f | |||
| ab5a1981d9 | |||
| eaf5e66f3a | |||
| 3e8b3b9327 | |||
| 7408558a55 | |||
| d87f878a44 | |||
| 7836d60c61 | |||
| c94287bbde | |||
| f909bc29ed | |||
| d736c4b180 | |||
| afe8dda7a9 | |||
| 558f919e4f | |||
| d7c9658c1c | |||
| 2a0b3cfdc2 | |||
| 1f53e047a3 | |||
| b0394c6d30 | |||
| d4d7730473 | |||
| 9dc98a6a2a | |||
| ffc73a2ffa | |||
| db5b236ad6 | |||
| bdd49619ae | |||
| 23803621e9 | |||
| 9520c6e6ed | |||
| c6e36108b3 | |||
| a9df9e91c6 | |||
| f9ffb1fc54 | |||
| 16ccfe3824 | |||
| 1b01e82d8d | |||
| f66c145ee8 | |||
| ad9575a041 | |||
| d71621a599 | |||
| 8fad60b1b4 | |||
| fde7910e85 | |||
| 66818ef89f | |||
| 1268373105 | |||
| a9b951e1fe | |||
| 28e72c7f7b | |||
| a669b4dbd0 | |||
| 8e7aa41bdf | |||
| 93ac76e9c7 | |||
| ff27abc06a | |||
| 04a378c39a | |||
| ecd598ab64 | |||
| 915f9b73be | |||
| cb756912ad | |||
| 253d32c274 | |||
| 0b471adc7d | |||
| 13ca445e13 | |||
| 87e61b643c | |||
| 6a6aa146ac | |||
| 1e61157c5c | |||
| 7c4bd4f2d6 | |||
| a81fc2d837 | |||
| 9c6cc829fa | |||
| 9eda3fa327 | |||
| 53b0223e20 | |||
| 15e80316fe | |||
| 485d5b86d0 | |||
| b0d5bff98f | |||
| 8c51316897 | |||
| 3bcfeafa15 | |||
| 3d461195e3 | |||
| e6a1fc448a | |||
| d1aefb38bb | |||
| e0a66fbe30 | |||
| 80db9efef5 | |||
| 8853e7a17e | |||
| ad38604390 | |||
| 64f4ee380d | |||
| 3bf88e2dd7 | |||
| c8508ade17 | |||
| 4e8db89403 | |||
| 1b8551d843 | |||
| 46ec8f758b | |||
| 46e49b745e | |||
| 4b29fab843 | |||
| 82e21622ef | |||
| e4dda36a1d | |||
| 1216fc1cbb | |||
| 2b4735e825 | |||
| 422e05d8b9 | |||
| 4c273e33c5 | |||
| b24fcbc800 | |||
| 852c10264b | |||
| ef59ee41a8 | |||
| da7febbfb9 |
+22
@@ -0,0 +1,22 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- tip
|
||||
|
||||
install: make deps
|
||||
script: make test
|
||||
|
||||
notifications:
|
||||
flowdock:
|
||||
secure: fZrcf9rlh2IrQrlch1sHkn3YI7SKvjGnAl/zyV5D6NROe1Bbr6d3QRMuCXWWdhJHzjKmXk5rIzbqJhUc0PNF7YjxGNKSzqWMQ56KcvN1k8DzlqxpqkcA3Jbs6fXCWo2fssRtZ7hj/wOP1f5n6cc7kzHDt9dgaYJ6nO2fqNPJiTc=
|
||||
|
||||
irc:
|
||||
channels:
|
||||
- "chat.freenode.net#packer-tool"
|
||||
on_success: change
|
||||
on_failure: always
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- go: tip
|
||||
+116
@@ -1,3 +1,119 @@
|
||||
## 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:
|
||||
|
||||
* provisioner/shell: New setting `start_retry_timeout` which is the timeout
|
||||
for the provisioner to attempt to _start_ the remote process. This allows
|
||||
the shell provisioner to work properly with reboots. [GH-260]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Remote command output containing '\r' now looks much better
|
||||
within the Packer output.
|
||||
* builder/vmware: Fix issue with finding driver files. [GH-279]
|
||||
* provisioner/salt-masterless: Uploads work properly from Windows. [GH-276]
|
||||
|
||||
## 0.3.0 (August 12, 2013)
|
||||
|
||||
BACKWARDS INCOMPATIBILITIES:
|
||||
|
||||
* All `{{.CreateTime}}` variables within templates (such as for AMI names)
|
||||
are now replaced with `{{timestamp}}`. Run `packer fix` to fix your
|
||||
templates.
|
||||
|
||||
FEATURES:
|
||||
|
||||
* **User Variables** allow you to specify variables within your templates
|
||||
that can be replaced using the command-line, files, or environmental
|
||||
variables. This dramatically improves the portability of packer templates.
|
||||
See the documentation for more information.
|
||||
* **Machine-readable output** can be enabled by passing the
|
||||
`-machine-readable` flag to _any_ Packer command.
|
||||
* All strings in a template are now processed for variables/functions,
|
||||
so things like `{{timestamp}}` can be used everywhere. More features will
|
||||
be added in the future.
|
||||
* The `amazon` builders (all of them) can now have attributes of their
|
||||
resulting AMIs modified, such as access permissions and product codes.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* builder/amazon/all: User data can be passed to start the instances. [GH-253]
|
||||
* provisioner/salt-masterless: `local_state_tree` is no longer required,
|
||||
allowing you to use shell provisioner (or others) to bring this down.
|
||||
[GH-269]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* builder/amazon/ebs,instance: Retry deleing security group a few times.
|
||||
[GH-278]
|
||||
* builder/vmware: Workstation works on Windows XP now. [GH-238]
|
||||
* builder/vmware: Look for files on Windows in multiple locations
|
||||
using multiple environmental variables. [GH-263]
|
||||
* provisioner/salt-masterless: states aren't deleted after the run
|
||||
anymore. [GH-265]
|
||||
* provisioner/salt-masterless: error if any commands exit with a non-zero
|
||||
exit status. [GH-266]
|
||||
|
||||
## 0.2.3 (August 7, 2013)
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* builder/amazon/all: Added Amazon AMI tag support [GH-233]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Absolute/relative filepaths on Windows now work for iso_url
|
||||
and other settings. [GH-240]
|
||||
* builder/amazon/all: instance info is refreshed while waiting for SSH,
|
||||
allowing Packer to see updated IP/DNS info. [GH-243]
|
||||
|
||||
## 0.2.2 (August 1, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -43,7 +43,7 @@ own.
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-example {{.CreateTime}}"
|
||||
"ami_name": "packer-example {{timestamp}}"
|
||||
}]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -10,11 +10,10 @@ import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"runtime"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
@@ -25,8 +24,8 @@ const BuilderId = "mitchellh.amazon.chroot"
|
||||
type Config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
ChrootMounts [][]string `mapstructure:"chroot_mounts"`
|
||||
CopyFiles []string `mapstructure:"copy_files"`
|
||||
DevicePath string `mapstructure:"device_path"`
|
||||
@@ -34,6 +33,8 @@ type Config struct {
|
||||
MountPath string `mapstructure:"mount_path"`
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
UnmountCommand string `mapstructure:"unmount_command"`
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -47,6 +48,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Defaults
|
||||
if b.config.ChrootMounts == nil {
|
||||
b.config.ChrootMounts = make([][]string, 0)
|
||||
@@ -84,31 +91,56 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
if b.config.AMIName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
for _, mounts := range b.config.ChrootMounts {
|
||||
for i, mounts := range b.config.ChrootMounts {
|
||||
if len(mounts) != 3 {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("Each chroot_mounts entry should be three elements."))
|
||||
break
|
||||
}
|
||||
|
||||
for j, entry := range mounts {
|
||||
b.config.ChrootMounts[i][j], err = b.config.tpl.Process(entry, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing chroot_mounts[%d][%d]: %s",
|
||||
i, j, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for i, file := range b.config.CopyFiles {
|
||||
var err error
|
||||
b.config.CopyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing copy_files[%d]: %s",
|
||||
i, err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.SourceAmi == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("source_ami is required."))
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"device_path": &b.config.DevicePath,
|
||||
"mount_command": &b.config.MountCommand,
|
||||
"source_ami": &b.config.SourceAmi,
|
||||
"unmount_command": &b.config.UnmountCommand,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
@@ -157,6 +189,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&StepEarlyCleanup{},
|
||||
&StepSnapshot{},
|
||||
&StepRegisterAMI{},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type mountPathData struct {
|
||||
@@ -30,14 +29,17 @@ func (s *StepMountDevice) Run(state map[string]interface{}) multistep.StepAction
|
||||
ui := state["ui"].(packer.Ui)
|
||||
device := state["device"].(string)
|
||||
|
||||
mountPathRaw := new(bytes.Buffer)
|
||||
t := template.Must(template.New("mountPath").Parse(config.MountPath))
|
||||
t.Execute(mountPathRaw, &mountPathData{
|
||||
mountPath, err := config.tpl.Process(config.MountPath, &mountPathData{
|
||||
Device: filepath.Base(device),
|
||||
})
|
||||
|
||||
var err error
|
||||
mountPath := mountPathRaw.String()
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing mount directory: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
mountPath, err = filepath.Abs(mountPath)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing mount directory: %s", err)
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package chroot
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
// StepRegisterAMI creates the AMI.
|
||||
type StepRegisterAMI struct{}
|
||||
|
||||
@@ -26,16 +18,6 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
snapshotId := state["snapshot_id"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
blockDevices := make([]ec2.BlockDeviceMapping, len(image.BlockDevices))
|
||||
for i, device := range image.BlockDevices {
|
||||
@@ -48,7 +30,7 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
}
|
||||
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
Name: amiName,
|
||||
Name: config.AMIName,
|
||||
Architecture: image.Architecture,
|
||||
KernelId: image.KernelId,
|
||||
RamdiskId: image.RamdiskId,
|
||||
|
||||
@@ -3,6 +3,7 @@ package common
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/aws"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
@@ -36,12 +37,40 @@ func (c *AccessConfig) Region() (aws.Region, error) {
|
||||
return aws.Regions[region], nil
|
||||
}
|
||||
|
||||
func (c *AccessConfig) Prepare() []error {
|
||||
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{
|
||||
"access_key": &c.AccessKey,
|
||||
"secret_key": &c.SecretKey,
|
||||
"region": &c.RawRegion,
|
||||
}
|
||||
|
||||
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.RawRegion != "" {
|
||||
if _, ok := aws.Regions[c.RawRegion]; !ok {
|
||||
return []error{fmt.Errorf("Unknown region: %s", c.RawRegion)}
|
||||
errs = append(errs, fmt.Errorf("Unknown region: %s", c.RawRegion))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -11,17 +11,17 @@ func testAccessConfig() *AccessConfig {
|
||||
func TestAccessConfigPrepare_Region(t *testing.T) {
|
||||
c := testAccessConfig()
|
||||
c.RawRegion = ""
|
||||
if err := c.Prepare(); err != nil {
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.RawRegion = "us-east-12"
|
||||
if err := c.Prepare(); err == nil {
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
c.RawRegion = "us-east-1"
|
||||
if err := c.Prepare(); err != nil {
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"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"`
|
||||
}
|
||||
|
||||
func (c *AMIConfig) 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{
|
||||
"ami_name": &c.AMIName,
|
||||
"ami_description": &c.AMIDescription,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
sliceTemplates := map[string][]string{
|
||||
"ami_users": c.AMIUsers,
|
||||
"ami_groups": c.AMIGroups,
|
||||
"ami_product_codes": c.AMIProductCodes,
|
||||
}
|
||||
|
||||
for n, slice := range sliceTemplates {
|
||||
for i, elem := range slice {
|
||||
var err error
|
||||
slice[i], err = t.Process(elem, nil)
|
||||
if err != nil {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Error processing %s[%d]: %s", n, i, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if c.AMIName == "" {
|
||||
errs = append(errs, fmt.Errorf("ami_name must be specified"))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testAMIConfig() *AMIConfig {
|
||||
return &AMIConfig{
|
||||
AMIName: "foo",
|
||||
}
|
||||
}
|
||||
|
||||
func TestAMIConfigPrepare_Region(t *testing.T) {
|
||||
c := testAMIConfig()
|
||||
if err := c.Prepare(nil); err != nil {
|
||||
t.Fatalf("shouldn't have err: %s", err)
|
||||
}
|
||||
|
||||
c.AMIName = ""
|
||||
if err := c.Prepare(nil); err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -9,18 +9,34 @@ import (
|
||||
"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 EC2 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, err error)
|
||||
|
||||
// StateChangeConf is the configuration struct used for `WaitForState`.
|
||||
type StateChangeConf struct {
|
||||
Conn *ec2.EC2
|
||||
Pending []string
|
||||
Refresh func() (interface{}, string, error)
|
||||
Refresh StateRefreshFunc
|
||||
StepState map[string]interface{}
|
||||
Target string
|
||||
}
|
||||
|
||||
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{}, string, error) {
|
||||
// InstanceStateRefreshFunc returns a StateRefreshFunc that is used to watch
|
||||
// an EC2 instance.
|
||||
func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) StateRefreshFunc {
|
||||
return func() (interface{}, string, error) {
|
||||
resp, err := conn.Instances([]string{i.InstanceId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
log.Printf("Error on InstanceStateRefresh: %s", err)
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
@@ -29,6 +45,8 @@ func InstanceStateRefreshFunc(conn *ec2.EC2, i *ec2.Instance) func() (interface{
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ package common
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -12,6 +14,8 @@ type RunConfig struct {
|
||||
SourceAmi string `mapstructure:"source_ami"`
|
||||
IamInstanceProfile string `mapstructure:"iam_instance_profile"`
|
||||
InstanceType string `mapstructure:"instance_type"`
|
||||
UserData string `mapstructure:"user_data"`
|
||||
UserDataFile string `mapstructure:"user_data_file"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort int `mapstructure:"ssh_port"`
|
||||
@@ -23,7 +27,15 @@ type RunConfig struct {
|
||||
sshTimeout time.Duration
|
||||
}
|
||||
|
||||
func (c *RunConfig) Prepare() []error {
|
||||
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.SSHPort == 0 {
|
||||
c.SSHPort = 22
|
||||
@@ -48,6 +60,34 @@ func (c *RunConfig) Prepare() []error {
|
||||
errs = append(errs, errors.New("An ssh_username must be specified"))
|
||||
}
|
||||
|
||||
if c.UserData != "" && c.UserDataFile != "" {
|
||||
errs = append(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
|
||||
} else if c.UserDataFile != "" {
|
||||
if _, err := os.Stat(c.UserDataFile); err != nil {
|
||||
errs = append(errs, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"iam_instance_profile": &c.IamInstanceProfile,
|
||||
"instance_type": &c.InstanceType,
|
||||
"ssh_timeout": &c.RawSSHTimeout,
|
||||
"security_group_id": &c.SecurityGroupId,
|
||||
"ssh_username": &c.SSHUsername,
|
||||
"source_ami": &c.SourceAmi,
|
||||
"subnet_id": &c.SubnetId,
|
||||
"vpc_id": &c.VpcId,
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
@@ -24,7 +25,7 @@ func testConfig() *RunConfig {
|
||||
|
||||
func TestRunConfigPrepare(t *testing.T) {
|
||||
c := testConfig()
|
||||
err := c.Prepare()
|
||||
err := c.Prepare(nil)
|
||||
if len(err) > 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
@@ -33,7 +34,7 @@ func TestRunConfigPrepare(t *testing.T) {
|
||||
func TestRunConfigPrepare_InstanceType(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.InstanceType = ""
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -41,7 +42,7 @@ func TestRunConfigPrepare_InstanceType(t *testing.T) {
|
||||
func TestRunConfigPrepare_SourceAmi(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SourceAmi = ""
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -49,7 +50,7 @@ func TestRunConfigPrepare_SourceAmi(t *testing.T) {
|
||||
func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SSHPort = 0
|
||||
if err := c.Prepare(); len(err) != 0 {
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
@@ -58,7 +59,7 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
}
|
||||
|
||||
c.SSHPort = 44
|
||||
if err := c.Prepare(); len(err) != 0 {
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
@@ -70,12 +71,12 @@ func TestRunConfigPrepare_SSHPort(t *testing.T) {
|
||||
func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.RawSSHTimeout = ""
|
||||
if err := c.Prepare(); len(err) != 0 {
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.RawSSHTimeout = "bad"
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
@@ -83,7 +84,45 @@ func TestRunConfigPrepare_SSHTimeout(t *testing.T) {
|
||||
func TestRunConfigPrepare_SSHUsername(t *testing.T) {
|
||||
c := testConfig()
|
||||
c.SSHUsername = ""
|
||||
if err := c.Prepare(); len(err) != 1 {
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_UserData(t *testing.T) {
|
||||
c := testConfig()
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
c.UserData = "foo"
|
||||
c.UserDataFile = tf.Name()
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunConfigPrepare_UserDataFile(t *testing.T) {
|
||||
c := testConfig()
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
c.UserDataFile = "idontexistidontthink"
|
||||
if err := c.Prepare(nil); len(err) != 1 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer tf.Close()
|
||||
|
||||
c.UserDataFile = tf.Name()
|
||||
if err := c.Prepare(nil); len(err) != 0 {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,23 +6,40 @@ import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/packer/communicator/ssh"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SSHAddress returns a function that can be given to the SSH communicator
|
||||
// for determining the SSH address based on the instance DNS name.
|
||||
func SSHAddress(port int) func(map[string]interface{}) (string, error) {
|
||||
func SSHAddress(e *ec2.EC2, port int) func(map[string]interface{}) (string, error) {
|
||||
return func(state map[string]interface{}) (string, error) {
|
||||
var host string
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
if instance.DNSName != "" {
|
||||
host = instance.DNSName
|
||||
} else if instance.VpcId == "" {
|
||||
host = instance.PrivateIpAddress
|
||||
} else {
|
||||
return "", errors.New("couldn't determine IP address for instance")
|
||||
for j := 0; j < 2; j++ {
|
||||
var host string
|
||||
i := state["instance"].(*ec2.Instance)
|
||||
if i.DNSName != "" {
|
||||
host = i.DNSName
|
||||
} else if i.VpcId != "" {
|
||||
host = i.PrivateIpAddress
|
||||
}
|
||||
|
||||
if host != "" {
|
||||
return fmt.Sprintf("%s:%d", host, port), nil
|
||||
}
|
||||
|
||||
r, err := e.Instances([]string{i.InstanceId}, ec2.NewFilter())
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(r.Reservations) == 0 || len(r.Reservations[0].Instances) == 0 {
|
||||
return "", fmt.Errorf("instance not found: %s", i.InstanceId)
|
||||
}
|
||||
|
||||
state["instance"] = &r.Reservations[0].Instances[0]
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s:%d", host, port), nil
|
||||
return "", errors.New("couldn't determine IP address for instance")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepCreateTags struct {
|
||||
Tags map[string]string
|
||||
}
|
||||
|
||||
func (s *StepCreateTags) 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.Tags) > 0 {
|
||||
ui.Say(fmt.Sprintf("Adding tags to AMI (%s)...", ami))
|
||||
|
||||
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 := ec2conn.CreateTags([]string{ami}, ec2Tags)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error adding tags to AMI (%s): %s", ami, err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *StepCreateTags) Cleanup(state map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
)
|
||||
|
||||
type StepModifyAMIAttributes struct {
|
||||
Users []string
|
||||
Groups []string
|
||||
ProductCodes []string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (s *StepModifyAMIAttributes) 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]
|
||||
|
||||
// Determine if there is any work to do.
|
||||
valid := false
|
||||
valid = valid || s.Description != ""
|
||||
valid = valid || (s.Users != nil && len(s.Users) > 0)
|
||||
valid = valid || (s.Groups != nil && len(s.Groups) > 0)
|
||||
valid = valid || (s.ProductCodes != nil && len(s.ProductCodes) > 0)
|
||||
|
||||
if !valid {
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
// 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...")
|
||||
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
|
||||
}
|
||||
|
||||
func (s *StepModifyAMIAttributes) Cleanup(state map[string]interface{}) {
|
||||
// No cleanup...
|
||||
}
|
||||
@@ -5,15 +5,19 @@ import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
)
|
||||
|
||||
type StepRunSourceInstance struct {
|
||||
ExpectedRootDevice string
|
||||
InstanceType string
|
||||
UserData string
|
||||
UserDataFile string
|
||||
SourceAMI string
|
||||
IamInstanceProfile string
|
||||
SubnetId string
|
||||
BlockDevices BlockDevices
|
||||
|
||||
instance *ec2.Instance
|
||||
}
|
||||
@@ -24,15 +28,28 @@ func (s *StepRunSourceInstance) Run(state map[string]interface{}) multistep.Step
|
||||
securityGroupId := state["securityGroupId"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
userData := s.UserData
|
||||
if s.UserDataFile != "" {
|
||||
contents, err := ioutil.ReadFile(s.UserDataFile)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Problem reading user data file: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
userData = string(contents)
|
||||
}
|
||||
|
||||
runOpts := &ec2.RunInstances{
|
||||
KeyName: keyName,
|
||||
ImageId: s.SourceAMI,
|
||||
InstanceType: s.InstanceType,
|
||||
UserData: []byte(userData),
|
||||
MinCount: 0,
|
||||
MaxCount: 0,
|
||||
SecurityGroups: []ec2.SecurityGroup{ec2.SecurityGroup{Id: securityGroupId}},
|
||||
IamInstanceProfile: s.IamInstanceProfile,
|
||||
SubnetId: s.SubnetId,
|
||||
BlockDevices: s.BlockDevices.BuildLaunchDevices(),
|
||||
}
|
||||
|
||||
ui.Say("Launching a source AWS instance...")
|
||||
@@ -75,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
|
||||
@@ -83,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
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"time"
|
||||
)
|
||||
|
||||
type StepSecurityGroup struct {
|
||||
@@ -83,9 +84,19 @@ func (s *StepSecurityGroup) Cleanup(state map[string]interface{}) {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Deleting temporary security group...")
|
||||
_, err := ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
|
||||
if err != nil {
|
||||
|
||||
var err error
|
||||
for i := 0; i < 5; i++ {
|
||||
_, err = ec2conn.DeleteSecurityGroup(ec2.SecurityGroup{Id: s.createdGroupId})
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("Error deleting security group: %s", err)
|
||||
time.Sleep(5 * time.Second)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf(
|
||||
"Error cleaning up security group. Please delete the group manually: %s", s.createdGroupId))
|
||||
}
|
||||
|
||||
@@ -6,15 +6,13 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
@@ -23,10 +21,14 @@ const BuilderId = "mitchellh.amazonebs"
|
||||
type config struct {
|
||||
common.PackerConfig `mapstructure:",squash"`
|
||||
awscommon.AccessConfig `mapstructure:",squash"`
|
||||
awscommon.AMIConfig `mapstructure:",squash"`
|
||||
awscommon.BlockDevices `mapstructure:",squash"`
|
||||
awscommon.RunConfig `mapstructure:",squash"`
|
||||
|
||||
// Configuration of the resulting AMI
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
// Tags for the AMI
|
||||
Tags map[string]string
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -40,23 +42,40 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
// Accumulate any errors
|
||||
if b.config.AMIName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
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("Failed parsing ami_name: %s", err))
|
||||
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
|
||||
}
|
||||
@@ -96,18 +115,27 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&awscommon.StepRunSourceInstance{
|
||||
ExpectedRootDevice: "ebs",
|
||||
InstanceType: b.config.InstanceType,
|
||||
UserData: b.config.UserData,
|
||||
UserDataFile: b.config.UserDataFile,
|
||||
SourceAMI: b.config.SourceAmi,
|
||||
IamInstanceProfile: b.config.IamInstanceProfile,
|
||||
SubnetId: b.config.SubnetId,
|
||||
BlockDevices: b.config.BlockDevices,
|
||||
},
|
||||
&common.StepConnectSSH{
|
||||
SSHAddress: awscommon.SSHAddress(b.config.SSHPort),
|
||||
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
|
||||
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
},
|
||||
&common.StepProvision{},
|
||||
&stepStopInstance{},
|
||||
&stepCreateAMI{},
|
||||
&awscommon.StepCreateTags{Tags: b.config.Tags},
|
||||
&awscommon.StepModifyAMIAttributes{
|
||||
Description: b.config.AMIDescription,
|
||||
Users: b.config.AMIUsers,
|
||||
Groups: b.config.AMIGroups,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -1,44 +1,27 @@
|
||||
package ebs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type stepCreateAMI struct{}
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
func (s *stepCreateAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
ec2conn := state["ec2"].(*ec2.EC2)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
// Create the image
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", amiName))
|
||||
ui.Say(fmt.Sprintf("Creating the AMI: %s", config.AMIName))
|
||||
createOpts := &ec2.CreateImage{
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: amiName,
|
||||
InstanceId: instance.InstanceId,
|
||||
Name: config.AMIName,
|
||||
BlockDevices: config.BlockDevices.BuildAMIDevices(),
|
||||
}
|
||||
|
||||
createResp, err := ec2conn.CreateImage(createOpts)
|
||||
|
||||
@@ -8,12 +8,11 @@ import (
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
// The unique ID for this builder
|
||||
@@ -24,18 +23,22 @@ const BuilderId = "mitchellh.amazon.instance"
|
||||
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"`
|
||||
AMIName string `mapstructure:"ami_name"`
|
||||
BundleDestination string `mapstructure:"bundle_destination"`
|
||||
BundlePrefix string `mapstructure:"bundle_prefix"`
|
||||
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 *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -49,12 +52,18 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
return err
|
||||
}
|
||||
|
||||
b.config.tpl, err = packer.NewConfigTemplate()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.config.tpl.UserVars = b.config.PackerUserVars
|
||||
|
||||
if b.config.BundleDestination == "" {
|
||||
b.config.BundleDestination = "/tmp"
|
||||
}
|
||||
|
||||
if b.config.BundlePrefix == "" {
|
||||
b.config.BundlePrefix = "image-{{.CreateTime}}"
|
||||
b.config.BundlePrefix = "image-{{timestamp}}"
|
||||
}
|
||||
|
||||
if b.config.BundleUploadCommand == "" {
|
||||
@@ -86,8 +95,41 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
|
||||
// Accumulate any errors
|
||||
errs := common.CheckUnusedConfig(md)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare()...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AccessConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.AMIConfig.Prepare(b.config.tpl)...)
|
||||
errs = packer.MultiErrorAppend(errs, b.config.RunConfig.Prepare(b.config.tpl)...)
|
||||
|
||||
validates := map[string]*string{
|
||||
"bundle_upload_command": &b.config.BundleUploadCommand,
|
||||
"bundle_vol_command": &b.config.BundleVolCommand,
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"account_id": &b.config.AccountId,
|
||||
"ami_name": &b.config.AMIName,
|
||||
"bundle_destination": &b.config.BundleDestination,
|
||||
"bundle_prefix": &b.config.BundlePrefix,
|
||||
"s3_bucket": &b.config.S3Bucket,
|
||||
"x509_cert_path": &b.config.X509CertPath,
|
||||
"x509_key_path": &b.config.X509KeyPath,
|
||||
"x509_upload_path": &b.config.X509UploadPath,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.AccountId == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("account_id is required"))
|
||||
@@ -95,17 +137,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.AccountId = strings.Replace(b.config.AccountId, "-", "", -1)
|
||||
}
|
||||
|
||||
if b.config.AMIName == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("ami_name must be specified"))
|
||||
} else {
|
||||
_, err = template.New("ami").Parse(b.config.AMIName)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing ami_name: %s", err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.S3Bucket == "" {
|
||||
errs = packer.MultiErrorAppend(errs, errors.New("s3_bucket is required"))
|
||||
}
|
||||
@@ -163,11 +194,15 @@ 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(b.config.SSHPort),
|
||||
SSHAddress: awscommon.SSHAddress(ec2conn, b.config.SSHPort),
|
||||
SSHConfig: awscommon.SSHConfig(b.config.SSHUsername),
|
||||
SSHWaitTimeout: b.config.SSHTimeout(),
|
||||
},
|
||||
@@ -176,6 +211,13 @@ 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,
|
||||
},
|
||||
}
|
||||
|
||||
// Run!
|
||||
|
||||
@@ -115,7 +115,7 @@ func TestBuilderPrepare_BundlePrefix(t *testing.T) {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.BundlePrefix != "image-{{.CreateTime}}" {
|
||||
if b.config.BundlePrefix == "" {
|
||||
t.Fatalf("bad: %s", b.config.BundlePrefix)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type bundleCmdData struct {
|
||||
@@ -21,10 +17,6 @@ type bundleCmdData struct {
|
||||
PrivatePath string
|
||||
}
|
||||
|
||||
type bundlePrefixData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
type StepBundleVolume struct{}
|
||||
|
||||
func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepAction {
|
||||
@@ -55,29 +47,26 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
|
||||
}
|
||||
|
||||
// Bundle the volume
|
||||
var bundlePrefix bytes.Buffer
|
||||
prefixTData := bundlePrefixData{
|
||||
CreateTime: strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
t := template.Must(template.New("bundlePrefix").Parse(config.BundlePrefix))
|
||||
t.Execute(&bundlePrefix, prefixTData)
|
||||
|
||||
var bundleCmd bytes.Buffer
|
||||
tData := bundleCmdData{
|
||||
var err error
|
||||
config.BundleVolCommand, err = config.tpl.Process(config.BundleVolCommand, bundleCmdData{
|
||||
AccountId: config.AccountId,
|
||||
Architecture: instance.Architecture,
|
||||
CertPath: x509RemoteCertPath,
|
||||
Destination: config.BundleDestination,
|
||||
KeyPath: x509RemoteKeyPath,
|
||||
Prefix: bundlePrefix.String(),
|
||||
Prefix: config.BundlePrefix,
|
||||
PrivatePath: config.X509UploadPath,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing bundle volume command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
t = template.Must(template.New("bundleCmd").Parse(config.BundleVolCommand))
|
||||
t.Execute(&bundleCmd, tData)
|
||||
|
||||
ui.Say("Bundling the volume...")
|
||||
cmd = new(packer.RemoteCmd)
|
||||
cmd.Command = bundleCmd.String()
|
||||
cmd.Command = config.BundleVolCommand
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state["error"] = fmt.Errorf("Error bundling volume: %s", err)
|
||||
ui.Error(state["error"].(error).Error())
|
||||
@@ -93,7 +82,7 @@ func (s *StepBundleVolume) Run(state map[string]interface{}) multistep.StepActio
|
||||
}
|
||||
|
||||
// Store the manifest path
|
||||
manifestName := bundlePrefix.String() + ".manifest.xml"
|
||||
manifestName := config.BundlePrefix + ".manifest.xml"
|
||||
state["manifest_name"] = manifestName
|
||||
state["manifest_path"] = fmt.Sprintf(
|
||||
"%s/%s", config.BundleDestination, manifestName)
|
||||
|
||||
@@ -1,21 +1,13 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/goamz/ec2"
|
||||
"github.com/mitchellh/multistep"
|
||||
awscommon "github.com/mitchellh/packer/builder/amazon/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
type amiNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
type StepRegisterAMI struct{}
|
||||
|
||||
func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction {
|
||||
@@ -24,20 +16,11 @@ func (s *StepRegisterAMI) Run(state map[string]interface{}) multistep.StepAction
|
||||
manifestPath := state["remote_manifest_path"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
// Parse the name of the AMI
|
||||
amiNameBuf := new(bytes.Buffer)
|
||||
tData := amiNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
|
||||
t := template.Must(template.New("ami").Parse(config.AMIName))
|
||||
t.Execute(amiNameBuf, tData)
|
||||
amiName := amiNameBuf.String()
|
||||
|
||||
ui.Say("Registering the AMI...")
|
||||
registerOpts := &ec2.RegisterImage{
|
||||
ImageLocation: manifestPath,
|
||||
Name: amiName,
|
||||
Name: config.AMIName,
|
||||
BlockDevices: config.BlockDevices.BuildAMIDevices(),
|
||||
}
|
||||
|
||||
registerResp, err := ec2conn.RegisterImage(registerOpts)
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
package instance
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type uploadCmdData struct {
|
||||
@@ -25,19 +23,23 @@ func (s *StepUploadBundle) Run(state map[string]interface{}) multistep.StepActio
|
||||
manifestPath := state["manifest_path"].(string)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
var uploadCmd bytes.Buffer
|
||||
tData := uploadCmdData{
|
||||
var err error
|
||||
config.BundleUploadCommand, err = config.tpl.Process(config.BundleUploadCommand, uploadCmdData{
|
||||
AccessKey: config.AccessKey,
|
||||
BucketName: config.S3Bucket,
|
||||
BundleDirectory: config.BundleDestination,
|
||||
ManifestPath: manifestPath,
|
||||
SecretKey: config.SecretKey,
|
||||
})
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error processing bundle upload command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
t := template.Must(template.New("uploadCmd").Parse(config.BundleUploadCommand))
|
||||
t.Execute(&uploadCmd, tData)
|
||||
|
||||
ui.Say("Uploading the bundle...")
|
||||
cmd := &packer.RemoteCmd{Command: uploadCmd.String()}
|
||||
cmd := &packer.RemoteCmd{Command: config.BundleUploadCommand}
|
||||
if err := cmd.StartWithUi(comm, ui); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading volume: %s", err)
|
||||
ui.Error(state["error"].(error).Error())
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
package common
|
||||
|
||||
// PackerConfig is a struct that contains the configuration keys that
|
||||
// are sent by packer, properly tagged already so mapstructure can load
|
||||
// them. Embed this structure into your configuration class to get it.
|
||||
type PackerConfig struct {
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
PackerForce bool `mapstructure:"packer_force"`
|
||||
}
|
||||
+25
-17
@@ -53,10 +53,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 +71,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 +98,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 +106,7 @@ 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
|
||||
}
|
||||
@@ -112,7 +114,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 +125,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 +141,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 +149,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 +169,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{}
|
||||
|
||||
|
||||
@@ -4,26 +4,19 @@
|
||||
package digitalocean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The unique id for the builder
|
||||
const BuilderId = "pearkes.digitalocean"
|
||||
|
||||
type snapshotNameData struct {
|
||||
CreateTime string
|
||||
}
|
||||
|
||||
// Configuration tells the builder the credentials
|
||||
// to use while communicating with DO and describes the image
|
||||
// you are creating
|
||||
@@ -36,11 +29,10 @@ type config struct {
|
||||
SizeID uint `mapstructure:"size_id"`
|
||||
ImageID uint `mapstructure:"image_id"`
|
||||
|
||||
SnapshotName string
|
||||
SnapshotName string `mapstructure:"snapshot_name"`
|
||||
SSHUsername string `mapstructure:"ssh_username"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
|
||||
RawSnapshotName string `mapstructure:"snapshot_name"`
|
||||
RawSSHTimeout string `mapstructure:"ssh_timeout"`
|
||||
RawEventDelay string `mapstructure:"event_delay"`
|
||||
RawStateTimeout string `mapstructure:"state_timeout"`
|
||||
@@ -50,6 +42,8 @@ type config struct {
|
||||
sshTimeout time.Duration
|
||||
eventDelay time.Duration
|
||||
stateTimeout time.Duration
|
||||
|
||||
tpl *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
type Builder struct {
|
||||
@@ -63,6 +57,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
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)
|
||||
|
||||
@@ -92,6 +92,11 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.ImageID = 284203
|
||||
}
|
||||
|
||||
if b.config.SnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.SnapshotName = "packer-{{timestamp}}"
|
||||
}
|
||||
|
||||
if b.config.SSHUsername == "" {
|
||||
// Default to "root". You can override this if your
|
||||
// SourceImage has a different user account then the DO default
|
||||
@@ -103,11 +108,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName == "" {
|
||||
// Default to packer-{{ unix timestamp (utc) }}
|
||||
b.config.RawSnapshotName = "packer-{{.CreateTime}}"
|
||||
}
|
||||
|
||||
if b.config.RawSSHTimeout == "" {
|
||||
// Default to 1 minute timeouts
|
||||
b.config.RawSSHTimeout = "1m"
|
||||
@@ -125,6 +125,25 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.RawStateTimeout = "6m"
|
||||
}
|
||||
|
||||
templates := map[string]*string{
|
||||
"client_id": &b.config.ClientID,
|
||||
"api_key": &b.config.APIKey,
|
||||
"snapshot_name": &b.config.SnapshotName,
|
||||
"ssh_username": &b.config.SSHUsername,
|
||||
"ssh_timeout": &b.config.RawSSHTimeout,
|
||||
"event_delay": &b.config.RawEventDelay,
|
||||
"state_timeout": &b.config.RawStateTimeout,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
// Required configurations that will display errors if not set
|
||||
if b.config.ClientID == "" {
|
||||
errs = packer.MultiErrorAppend(
|
||||
@@ -157,20 +176,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
b.config.stateTimeout = stateTimeout
|
||||
|
||||
// Parse the name of the snapshot
|
||||
snapNameBuf := new(bytes.Buffer)
|
||||
tData := snapshotNameData{
|
||||
strconv.FormatInt(time.Now().UTC().Unix(), 10),
|
||||
}
|
||||
t, err := template.New("snapshot").Parse(b.config.RawSnapshotName)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed parsing snapshot_name: %s", err))
|
||||
} else {
|
||||
t.Execute(snapNameBuf, tData)
|
||||
b.config.SnapshotName = snapNameBuf.String()
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
return errs
|
||||
}
|
||||
|
||||
@@ -332,8 +332,8 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.RawSnapshotName != "packer-{{.CreateTime}}" {
|
||||
t.Errorf("invalid: %d", b.config.RawSnapshotName)
|
||||
if b.config.SnapshotName == "" {
|
||||
t.Errorf("invalid: %s", b.config.SnapshotName)
|
||||
}
|
||||
|
||||
// Test set
|
||||
@@ -345,7 +345,7 @@ func TestBuilderPrepare_SnapshotName(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test set with template
|
||||
config["snapshot_name"] = "{{.CreateTime}}"
|
||||
config["snapshot_name"] = "{{timestamp}}"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
|
||||
+109
-22
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
@@ -18,7 +18,6 @@ const BuilderId = "mitchellh.virtualbox"
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
driver Driver
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
@@ -38,7 +37,7 @@ 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"`
|
||||
@@ -51,12 +50,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 *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
@@ -65,6 +66,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
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)
|
||||
|
||||
@@ -124,6 +131,72 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.VMName = fmt.Sprintf("packer-%s", b.config.PackerBuildName)
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"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.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,
|
||||
"boot_wait": &b.config.RawBootWait,
|
||||
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, file := range b.config.FloppyFiles {
|
||||
var err error
|
||||
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing floppy_files[%d]: %s",
|
||||
i, err))
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
@@ -148,14 +221,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,14 +243,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(
|
||||
@@ -215,10 +287,13 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
errs, fmt.Errorf("Failed parsing ssh_wait_timeout: %s", err))
|
||||
}
|
||||
|
||||
b.driver, err = b.newDriver()
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Failed creating VirtualBox driver: %s", err))
|
||||
for i, args := range b.config.VBoxManage {
|
||||
for j, arg := range args {
|
||||
if err := b.config.tpl.Validate(arg); err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing vboxmanage[%d][%d]: %s", i, j, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if errs != nil && len(errs.Errors) > 0 {
|
||||
@@ -229,9 +304,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,
|
||||
@@ -262,7 +349,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
|
||||
|
||||
|
||||
@@ -84,6 +84,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 +211,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 +232,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 +241,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)
|
||||
@@ -269,6 +273,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 +297,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 +309,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 +319,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 +386,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 +394,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 +414,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 +428,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 +436,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,6 +445,7 @@ 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)
|
||||
@@ -403,12 +457,14 @@ func TestBuilderPrepare_SSHUser(t *testing.T) {
|
||||
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 +488,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 +496,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)
|
||||
|
||||
@@ -2,18 +2,15 @@ package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"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/builder/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{}) {}
|
||||
@@ -1,13 +1,11 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -49,11 +47,15 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
|
||||
ui.Say("Typing the boot command...")
|
||||
for _, command := range config.BootCommand {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("boot").Parse(command))
|
||||
t.Execute(&buf, tplData)
|
||||
command, err := config.tpl.Process(command, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
for _, code := range scancodes(buf.String()) {
|
||||
for _, code := range scancodes(command) {
|
||||
if code == "wait" {
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type guestAdditionsPathTemplate struct {
|
||||
@@ -39,12 +37,16 @@ func (s *stepUploadGuestAdditions) Run(state map[string]interface{}) multistep.S
|
||||
Version: version,
|
||||
}
|
||||
|
||||
var processedPath bytes.Buffer
|
||||
t := template.Must(template.New("path").Parse(config.GuestAdditionsPath))
|
||||
t.Execute(&processedPath, tplData)
|
||||
config.GuestAdditionsPath, err = config.tpl.Process(config.GuestAdditionsPath, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing guest additions path: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
ui.Say("Uploading VirtualBox guest additions ISO...")
|
||||
if err := comm.Upload(processedPath.String(), f); err != nil {
|
||||
if err := comm.Upload(config.GuestAdditionsPath, f); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading guest additions: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package virtualbox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type commandTemplate struct {
|
||||
@@ -40,10 +38,14 @@ func (s *stepVBoxManage) Run(state map[string]interface{}) multistep.StepAction
|
||||
copy(command, originalCommand)
|
||||
|
||||
for i, arg := range command {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("arg").Parse(arg))
|
||||
t.Execute(&buf, tplData)
|
||||
command[i] = buf.String()
|
||||
var err error
|
||||
command[i], err = config.tpl.Process(arg, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing vboxmanage command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
|
||||
ui.Message(fmt.Sprintf("Executing: %s", strings.Join(command, " ")))
|
||||
|
||||
+111
-16
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"math/rand"
|
||||
@@ -19,7 +19,6 @@ const BuilderId = "mitchellh.vmware"
|
||||
|
||||
type Builder struct {
|
||||
config config
|
||||
driver Driver
|
||||
runner multistep.Runner
|
||||
}
|
||||
|
||||
@@ -32,7 +31,7 @@ type config struct {
|
||||
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"`
|
||||
@@ -52,12 +51,14 @@ type config struct {
|
||||
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 *packer.ConfigTemplate
|
||||
}
|
||||
|
||||
func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
@@ -66,6 +67,12 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
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)
|
||||
|
||||
@@ -121,6 +128,81 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
b.config.ToolsUploadPath = "{{ .Flavor }}.iso"
|
||||
}
|
||||
|
||||
// Errors
|
||||
templates := map[string]*string{
|
||||
"disk_name": &b.config.DiskName,
|
||||
"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.RawSingleISOUrl,
|
||||
"output_directory": &b.config.OutputDir,
|
||||
"shutdown_command": &b.config.ShutdownCommand,
|
||||
"ssh_password": &b.config.SSHPassword,
|
||||
"ssh_username": &b.config.SSHUser,
|
||||
"tools_upload_flavor": &b.config.ToolsUploadFlavor,
|
||||
"vm_name": &b.config.VMName,
|
||||
"boot_wait": &b.config.RawBootWait,
|
||||
"shutdown_timeout": &b.config.RawShutdownTimeout,
|
||||
"ssh_wait_timeout": &b.config.RawSSHWaitTimeout,
|
||||
}
|
||||
|
||||
for n, ptr := range templates {
|
||||
var err error
|
||||
*ptr, err = b.config.tpl.Process(*ptr, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, fmt.Errorf("Error processing %s: %s", n, err))
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
fmt.Errorf("Error processing boot_command[%d]: %s", i, err))
|
||||
}
|
||||
}
|
||||
|
||||
for i, file := range b.config.FloppyFiles {
|
||||
var err error
|
||||
b.config.FloppyFiles[i], err = b.config.tpl.Process(file, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing floppy_files[%d]: %s",
|
||||
i, err))
|
||||
}
|
||||
}
|
||||
|
||||
newVMXData := make(map[string]string)
|
||||
for k, v := range b.config.VMXData {
|
||||
k, err = b.config.tpl.Process(k, nil)
|
||||
if err != nil {
|
||||
errs = packer.MultiErrorAppend(errs,
|
||||
fmt.Errorf("Error processing VMX data 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 VMX data value '%s': %s", v, err))
|
||||
continue
|
||||
}
|
||||
|
||||
newVMXData[k] = v
|
||||
}
|
||||
|
||||
b.config.VMXData = newVMXData
|
||||
|
||||
if b.config.HTTPPortMin > b.config.HTTPPortMax {
|
||||
errs = packer.MultiErrorAppend(
|
||||
errs, errors.New("http_port_min must be less than http_port_max"))
|
||||
@@ -145,14 +227,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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -207,12 +296,6 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
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
|
||||
}
|
||||
@@ -221,12 +304,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,
|
||||
@@ -254,7 +349,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
|
||||
|
||||
|
||||
@@ -52,6 +52,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 +72,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 +96,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 +108,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 +206,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 +215,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 +237,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 +304,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 +312,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,6 +332,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)
|
||||
@@ -300,6 +350,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 +398,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 +422,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,6 +430,7 @@ 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)
|
||||
@@ -397,6 +451,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 +460,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)
|
||||
|
||||
@@ -135,3 +135,7 @@ func (d *Workstation9Driver) ToolsIsoPath(flavor string) string {
|
||||
func (d *Workstation9Driver) DhcpLeasesPath(device string) string {
|
||||
return workstationDhcpLeasesPath(device)
|
||||
}
|
||||
|
||||
func (d *Workstation9Driver) VmnetnatConfPath() string {
|
||||
return workstationVmnetnatConfPath()
|
||||
}
|
||||
|
||||
@@ -41,3 +41,7 @@ func workstationDhcpLeasesPath(device string) string {
|
||||
func workstationToolsIsoPath(flavor string) string {
|
||||
return "/usr/lib/vmware/isoimages/" + flavor + ".iso"
|
||||
}
|
||||
|
||||
func workstationVmnetnatConfPath() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -23,12 +23,7 @@ func workstationFindVdiskManager() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
path, err = workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(path, "vmware-vdiskmanager.exe"), nil
|
||||
return findFile("vmware-vdiskmanager.exe", workstationProgramFilePaths()), nil
|
||||
}
|
||||
|
||||
func workstationFindVMware() (string, error) {
|
||||
@@ -37,12 +32,7 @@ func workstationFindVMware() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
path, err = workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(path, "vmware.exe"), nil
|
||||
return findFile("vmware.exe", workstationProgramFilePaths()), nil
|
||||
}
|
||||
|
||||
func workstationFindVmrun() (string, error) {
|
||||
@@ -51,30 +41,26 @@ func workstationFindVmrun() (string, error) {
|
||||
return path, nil
|
||||
}
|
||||
|
||||
path, err = workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(path, "vmrun.exe"), nil
|
||||
return findFile("vmrun.exe", workstationProgramFilePaths()), nil
|
||||
}
|
||||
|
||||
func workstationToolsIsoPath(flavor string) string {
|
||||
path, err := workstationVMwareRoot()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return filepath.Join(path, flavor+".iso")
|
||||
return findFile(flavor+".iso", workstationProgramFilePaths())
|
||||
}
|
||||
|
||||
func workstationDhcpLeasesPath(device string) string {
|
||||
programData := os.Getenv("ProgramData")
|
||||
if programData == "" {
|
||||
return ""
|
||||
path, err := workstationDhcpLeasesPathRegistry()
|
||||
if err != nil {
|
||||
log.Printf("Error finding leases in registry: %s", err)
|
||||
} else if _, err := os.Stat(path); err == nil {
|
||||
return path
|
||||
}
|
||||
|
||||
return filepath.Join(programData, "/VMware/vmnetdhcp.leases")
|
||||
return findFile("vmnetdhcp.leases", workstationDataFilePaths())
|
||||
}
|
||||
|
||||
func workstationVmnetnatConfPath() string {
|
||||
return findFile("vmnetnat.conf", workstationDataFilePaths())
|
||||
}
|
||||
|
||||
// See http://blog.natefinch.com/2012/11/go-win-stuff.html
|
||||
@@ -126,6 +112,105 @@ func workstationVMwareRoot() (s string, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
s = strings.Replace(s, "\\", "/", -1)
|
||||
return
|
||||
return normalizePath(s), nil
|
||||
}
|
||||
|
||||
// This reads the VMware DHCP leases path from the Windows registry.
|
||||
func workstationDhcpLeasesPathRegistry() (s string, err error) {
|
||||
key := "SYSTEM\\CurrentControlSet\\services\\VMnetDHCP\\Parameters"
|
||||
subkey := "LeaseFile"
|
||||
s, err = readRegString(syscall.HKEY_LOCAL_MACHINE, key, subkey)
|
||||
if err != nil {
|
||||
log.Printf(`Unable to read registry key %s\%s`, key, subkey)
|
||||
return
|
||||
}
|
||||
|
||||
return normalizePath(s), nil
|
||||
}
|
||||
|
||||
func normalizePath(path string) string {
|
||||
path = strings.Replace(path, "\\", "/", -1)
|
||||
path = strings.Replace(path, "//", "/", -1)
|
||||
path = strings.TrimRight(path, "/")
|
||||
return path
|
||||
}
|
||||
|
||||
func findFile(file string, paths []string) string {
|
||||
for _, path := range paths {
|
||||
path = filepath.Join(path, file)
|
||||
path = normalizePath(path)
|
||||
log.Printf("Searching for file '%s'", path)
|
||||
|
||||
if _, err := os.Stat(path); err == nil {
|
||||
log.Printf("Found file '%s'", path)
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("File not found: '%s'", file)
|
||||
return ""
|
||||
}
|
||||
|
||||
// workstationProgramFilesPaths returns a list of paths that are eligible
|
||||
// to contain program files we may want just as vmware.exe.
|
||||
func workstationProgramFilePaths() []string {
|
||||
path, err := workstationVMwareRoot()
|
||||
if err != nil {
|
||||
log.Printf("Error finding VMware root: %s", err)
|
||||
}
|
||||
|
||||
paths := make([]string, 0, 5)
|
||||
if os.Getenv("VMWARE_HOME") != "" {
|
||||
paths = append(paths, os.Getenv("VMWARE_HOME"))
|
||||
}
|
||||
|
||||
if path != "" {
|
||||
paths = append(paths, path)
|
||||
}
|
||||
|
||||
if os.Getenv("ProgramFiles(x86)") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ProgramFiles(x86)"), "/VMware/VMware Workstation"))
|
||||
}
|
||||
|
||||
if os.Getenv("ProgramFiles") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ProgramFiles"), "/VMware/VMware Workstation"))
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
// workstationDataFilePaths returns a list of paths that are eligible
|
||||
// to contain data files we may want such as vmnet NAT configuration files.
|
||||
func workstationDataFilePaths() []string {
|
||||
leasesPath, err := workstationDhcpLeasesPathRegistry()
|
||||
if err != nil {
|
||||
log.Printf("Error getting DHCP leases path: %s", err)
|
||||
}
|
||||
|
||||
if leasesPath != "" {
|
||||
leasesPath = filepath.Dir(leasesPath)
|
||||
}
|
||||
|
||||
paths := make([]string, 0, 5)
|
||||
if os.Getenv("VMWARE_DATA") != "" {
|
||||
paths = append(paths, os.Getenv("VMWARE_DATA"))
|
||||
}
|
||||
|
||||
if leasesPath != "" {
|
||||
paths = append(paths, leasesPath)
|
||||
}
|
||||
|
||||
if os.Getenv("ProgramData") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ProgramData"), "/VMware"))
|
||||
}
|
||||
|
||||
if os.Getenv("ALLUSERSPROFILE") != "" {
|
||||
paths = append(paths,
|
||||
filepath.Join(os.Getenv("ALLUSERSPROFILE"), "/Application Data/VMware"))
|
||||
}
|
||||
|
||||
return paths
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
@@ -17,15 +16,15 @@ import (
|
||||
type VMnetNatConfIPFinder struct{}
|
||||
|
||||
func (*VMnetNatConfIPFinder) HostIP() (string, error) {
|
||||
programData := os.Getenv("ProgramData")
|
||||
if programData == "" {
|
||||
return "", errors.New("ProgramData directory not found.")
|
||||
driver := &Workstation9Driver{}
|
||||
|
||||
vmnetnat := driver.VmnetnatConfPath()
|
||||
if vmnetnat == "" {
|
||||
return "", errors.New("Could not find NAT vmnet conf file")
|
||||
}
|
||||
|
||||
programData = strings.Replace(programData, "\\", "/", -1)
|
||||
vmnetnat := filepath.Join(programData, "/VMware/vmnetnat.conf")
|
||||
if _, err := os.Stat(vmnetnat); err != nil {
|
||||
return "", fmt.Errorf("Error with vmnetnat.conf: %s", err)
|
||||
return "", fmt.Errorf("Could not find NAT vmnet conf file: %s", vmnetnat)
|
||||
}
|
||||
|
||||
f, err := os.Open(vmnetnat)
|
||||
@@ -62,5 +61,5 @@ func (*VMnetNatConfIPFinder) HostIP() (string, error) {
|
||||
}
|
||||
}
|
||||
|
||||
return "", errors.New("host IP not found in NAT config")
|
||||
return "", errors.New("host IP not found in " + vmnetnat)
|
||||
}
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/builder/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{}) {}
|
||||
@@ -1,7 +1,6 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/go-vnc"
|
||||
"github.com/mitchellh/multistep"
|
||||
@@ -10,7 +9,6 @@ import (
|
||||
"net"
|
||||
"runtime"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
@@ -90,11 +88,21 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
|
||||
ui.Say("Typing the boot command over VNC...")
|
||||
for _, command := range config.BootCommand {
|
||||
var buf bytes.Buffer
|
||||
t := template.Must(template.New("boot").Parse(command))
|
||||
t.Execute(&buf, tplData)
|
||||
command, err := config.tpl.Process(command, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing boot command: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
vncSendString(c, buf.String())
|
||||
// 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)
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
type toolsUploadPathTemplate struct {
|
||||
@@ -34,11 +32,15 @@ func (*stepUploadTools) Run(state map[string]interface{}) multistep.StepAction {
|
||||
defer f.Close()
|
||||
|
||||
tplData := &toolsUploadPathTemplate{Flavor: config.ToolsUploadFlavor}
|
||||
var processedPath bytes.Buffer
|
||||
t := template.Must(template.New("path").Parse(config.ToolsUploadPath))
|
||||
t.Execute(&processedPath, tplData)
|
||||
config.ToolsUploadPath, err = config.tpl.Process(config.ToolsUploadPath, tplData)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error preparing upload path: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
if err := comm.Upload(processedPath.String(), f); err != nil {
|
||||
if err := comm.Upload(config.ToolsUploadPath, f); err != nil {
|
||||
state["error"] = fmt.Errorf("Error uploading VMware Tools: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
+49
-59
@@ -4,11 +4,12 @@ import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
cmdcommon "github.com/mitchellh/packer/common/command"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
@@ -22,15 +23,13 @@ func (Command) Help() string {
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
var cfgDebug bool
|
||||
var cfgForce bool
|
||||
var cfgExcept []string
|
||||
var cfgOnly []string
|
||||
buildOptions := new(cmdcommon.BuildOptions)
|
||||
|
||||
cmdFlags := flag.NewFlagSet("build", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
cmdFlags.BoolVar(&cfgDebug, "debug", false, "debug mode for builds")
|
||||
cmdFlags.BoolVar(&cfgForce, "force", false, "force a build if artifacts exist")
|
||||
cmdFlags.Var((*stringSliceValue)(&cfgExcept), "except", "build all builds except these")
|
||||
cmdFlags.Var((*stringSliceValue)(&cfgOnly), "only", "only build the given builds by name")
|
||||
cmdcommon.BuildOptionFlags(cmdFlags, buildOptions)
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
@@ -41,23 +40,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
if len(cfgOnly) > 0 && len(cfgExcept) > 0 {
|
||||
env.Ui().Error("Only one of '-except' or '-only' may be specified.\n")
|
||||
if err := buildOptions.Validate(); err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
userVars, err := buildOptions.AllUserVars()
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Error compiling user variables: %s", err))
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read the file into a byte array so that we can parse the template
|
||||
log.Printf("Reading template: %s", args[0])
|
||||
tplData, err := ioutil.ReadFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to read template file: %s", err))
|
||||
return 1
|
||||
}
|
||||
|
||||
// Parse the template into a machine-usable format
|
||||
log.Println("Parsing template...")
|
||||
tpl, err := packer.ParseTemplate(tplData)
|
||||
tpl, err := packer.ParseTemplateFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
|
||||
return 1
|
||||
@@ -72,47 +72,10 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
}
|
||||
|
||||
// Go through each builder and compile the builds that we care about
|
||||
buildNames := tpl.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
if len(cfgExcept) > 0 {
|
||||
found := false
|
||||
for _, only := range cfgExcept {
|
||||
if buildName == only {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
log.Printf("Skipping build '%s' because specified by -except.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(cfgOnly) > 0 {
|
||||
found := false
|
||||
for _, only := range cfgOnly {
|
||||
if buildName == only {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Printf("Skipping build '%s' because not specified by -only.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Creating build: %s", buildName)
|
||||
build, err := tpl.Build(buildName, components)
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to create build '%s': \n\n%s", buildName, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
builds = append(builds, build)
|
||||
builds, err := buildOptions.Builds(tpl, components)
|
||||
if err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
if cfgDebug {
|
||||
@@ -150,7 +113,7 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
log.Printf("Preparing build: %s", b.Name())
|
||||
b.SetDebug(cfgDebug)
|
||||
b.SetForce(cfgForce)
|
||||
err := b.Prepare()
|
||||
err := b.Prepare(userVars)
|
||||
if err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
return 1
|
||||
@@ -233,7 +196,16 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
if len(artifacts) > 0 {
|
||||
env.Ui().Say("\n==> Builds finished. The artifacts of successful builds are:")
|
||||
for name, buildArtifacts := range artifacts {
|
||||
for _, artifact := range buildArtifacts {
|
||||
// Create a UI for the machine readable stuff to be targetted
|
||||
ui := &packer.TargettedUi{
|
||||
Target: name,
|
||||
Ui: env.Ui(),
|
||||
}
|
||||
|
||||
// Machine-readable helpful
|
||||
ui.Machine("artifact-count", strconv.FormatInt(int64(len(buildArtifacts)), 10))
|
||||
|
||||
for i, artifact := range buildArtifacts {
|
||||
var message bytes.Buffer
|
||||
fmt.Fprintf(&message, "--> %s: ", name)
|
||||
|
||||
@@ -243,6 +215,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
fmt.Fprint(&message, "<nothing>")
|
||||
}
|
||||
|
||||
iStr := strconv.FormatInt(int64(i), 10)
|
||||
if artifact != nil {
|
||||
ui.Machine("artifact", iStr, "builder-id", artifact.BuilderId())
|
||||
ui.Machine("artifact", iStr, "id", artifact.Id())
|
||||
ui.Machine("artifact", iStr, "string", artifact.String())
|
||||
|
||||
files := artifact.Files()
|
||||
ui.Machine("artifact",
|
||||
iStr,
|
||||
"files-count", strconv.FormatInt(int64(len(files)), 10))
|
||||
for fi, file := range files {
|
||||
fiStr := strconv.FormatInt(int64(fi), 10)
|
||||
ui.Machine("artifact", iStr, "file", fiStr, file)
|
||||
}
|
||||
} else {
|
||||
ui.Machine("artifact", iStr, "nil")
|
||||
}
|
||||
|
||||
env.Ui().Say(message.String())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
func testEnvironment() packer.Environment {
|
||||
config := packer.DefaultEnvironmentConfig()
|
||||
config.Ui = &packer.ReaderWriterUi{
|
||||
config.Ui = &packer.BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
@@ -10,6 +10,9 @@ Options:
|
||||
|
||||
-debug Debug mode enabled for builds
|
||||
-force Force a build to continue if artifacts exist, deletes existing artifacts
|
||||
-machine-readable Machine-readable output
|
||||
-except=foo,bar,baz Build all builds other than these
|
||||
-only=foo,bar,baz Only build the given builds by name
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
`
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package build
|
||||
|
||||
import "strings"
|
||||
|
||||
type stringSliceValue []string
|
||||
|
||||
func (s *stringSliceValue) String() string {
|
||||
return strings.Join(*s, ",")
|
||||
}
|
||||
|
||||
func (s *stringSliceValue) Set(value string) error {
|
||||
*s = strings.Split(value, ",")
|
||||
return nil
|
||||
}
|
||||
+13
-2
@@ -6,6 +6,7 @@ import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
@@ -49,10 +50,20 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
tplF.Close()
|
||||
|
||||
// Run the template through the various fixers
|
||||
fixers := []Fixer{Fixers["iso-md5"]}
|
||||
fixers := []string{
|
||||
"iso-md5",
|
||||
"createtime",
|
||||
}
|
||||
|
||||
input := templateData
|
||||
for _, fixer := range fixers {
|
||||
for _, name := range fixers {
|
||||
var err error
|
||||
fixer, ok := Fixers[name]
|
||||
if !ok {
|
||||
panic("fixer not found: " + name)
|
||||
}
|
||||
|
||||
log.Printf("Running fixer: %s", name)
|
||||
input, err = fixer.Fix(input)
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Error fixing: %s", err))
|
||||
|
||||
@@ -13,6 +13,7 @@ var Fixers map[string]Fixer
|
||||
|
||||
func init() {
|
||||
Fixers = map[string]Fixer{
|
||||
"iso-md5": new(FixerISOMD5),
|
||||
"iso-md5": new(FixerISOMD5),
|
||||
"createtime": new(FixerCreateTime),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// FixerCreateTime is a Fixer that replaces the ".CreateTime" template
|
||||
// calls with "{{timestamp}"
|
||||
type FixerCreateTime struct{}
|
||||
|
||||
func (FixerCreateTime) Fix(input map[string]interface{}) (map[string]interface{}, error) {
|
||||
// Our template type we'll use for this fixer only
|
||||
type template struct {
|
||||
Builders []map[string]interface{}
|
||||
}
|
||||
|
||||
// Decode the input into our structure, if we can
|
||||
var tpl template
|
||||
if err := mapstructure.Decode(input, &tpl); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
badKeys := []string{
|
||||
"ami_name",
|
||||
"bundle_prefix",
|
||||
"snapshot_name",
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`{{\s*\.CreateTime\s*}}`)
|
||||
|
||||
// Go through each builder and replace CreateTime if we can
|
||||
for _, builder := range tpl.Builders {
|
||||
for _, key := range badKeys {
|
||||
raw, ok := builder[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
v, ok := raw.(string)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
builder[key] = re.ReplaceAllString(v, "{{timestamp}}")
|
||||
}
|
||||
}
|
||||
|
||||
input["builders"] = tpl.Builders
|
||||
return input, nil
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package fix
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFixerCreateTime_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(FixerCreateTime)
|
||||
if _, ok := raw.(Fixer); !ok {
|
||||
t.Fatalf("must be a Fixer")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFixerCreateTime_Fix(t *testing.T) {
|
||||
var f FixerCreateTime
|
||||
|
||||
input := map[string]interface{}{
|
||||
"builders": []interface{}{
|
||||
map[string]string{
|
||||
"type": "foo",
|
||||
"ami_name": "{{.CreateTime}} foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
expected := map[string]interface{}{
|
||||
"builders": []map[string]interface{}{
|
||||
map[string]interface{}{
|
||||
"type": "foo",
|
||||
"ami_name": "{{timestamp}} foo",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
output, err := f.Fix(input)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(output, expected) {
|
||||
t.Fatalf("unexpected: %#v\nexpected: %#v\n", output, expected)
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
`
|
||||
+21
-19
@@ -3,8 +3,8 @@ package validate
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
cmdcommon "github.com/mitchellh/packer/common/command"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
@@ -17,10 +17,12 @@ func (Command) Help() string {
|
||||
|
||||
func (c Command) Run(env packer.Environment, args []string) int {
|
||||
var cfgSyntaxOnly bool
|
||||
buildOptions := new(cmdcommon.BuildOptions)
|
||||
|
||||
cmdFlags := flag.NewFlagSet("validate", flag.ContinueOnError)
|
||||
cmdFlags.Usage = func() { env.Ui().Say(c.Help()) }
|
||||
cmdFlags.BoolVar(&cfgSyntaxOnly, "syntax-only", false, "check syntax only")
|
||||
cmdcommon.BuildOptionFlags(cmdFlags, buildOptions)
|
||||
if err := cmdFlags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
@@ -31,17 +33,24 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Read the file into a byte array so that we can parse the template
|
||||
log.Printf("Reading template: %s", args[0])
|
||||
tplData, err := ioutil.ReadFile(args[0])
|
||||
if err := buildOptions.Validate(); err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
userVars, err := buildOptions.AllUserVars()
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to read template file: %s", err))
|
||||
env.Ui().Error(fmt.Sprintf("Error compiling user variables: %s", err))
|
||||
env.Ui().Error("")
|
||||
env.Ui().Error(c.Help())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Parse the template into a machine-usable format
|
||||
log.Println("Parsing template...")
|
||||
tpl, err := packer.ParseTemplate(tplData)
|
||||
log.Printf("Reading template: %s", args[0])
|
||||
tpl, err := packer.ParseTemplateFile(args[0])
|
||||
if err != nil {
|
||||
env.Ui().Error(fmt.Sprintf("Failed to parse template: %s", err))
|
||||
return 1
|
||||
@@ -63,23 +72,16 @@ func (c Command) Run(env packer.Environment, args []string) int {
|
||||
}
|
||||
|
||||
// Otherwise, get all the builds
|
||||
buildNames := tpl.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
log.Printf("Creating build from template for: %s", buildName)
|
||||
build, err := tpl.Build(buildName, components)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Build '%s': %s", buildName, err))
|
||||
continue
|
||||
}
|
||||
|
||||
builds = append(builds, build)
|
||||
builds, err := buildOptions.Builds(tpl, components)
|
||||
if err != nil {
|
||||
env.Ui().Error(err.Error())
|
||||
return 1
|
||||
}
|
||||
|
||||
// Check the configuration of all builds
|
||||
for _, b := range builds {
|
||||
log.Printf("Preparing build: %s", b.Name())
|
||||
err := b.Prepare()
|
||||
err := b.Prepare(userVars)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("Errors validating build '%s'. %s", b.Name(), err))
|
||||
}
|
||||
|
||||
@@ -12,5 +12,9 @@ Usage: packer validate [options] TEMPLATE
|
||||
|
||||
Options:
|
||||
|
||||
-syntax-only Only check syntax. Do not verify config of the template.
|
||||
-syntax-only Only check syntax. Do not verify config of the template.
|
||||
-except=foo,bar,baz Validate all builds other than these
|
||||
-only=foo,bar,baz Validate only these builds
|
||||
-var 'key=value' Variable for templates, can be used multiple times.
|
||||
-var-file=path JSON file containing user variables.
|
||||
`
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// BuildOptionFlags sets the proper command line flags needed for
|
||||
// build options.
|
||||
func BuildOptionFlags(fs *flag.FlagSet, f *BuildOptions) {
|
||||
fs.Var((*SliceValue)(&f.Except), "except", "build all builds except these")
|
||||
fs.Var((*SliceValue)(&f.Only), "only", "only build the given builds by name")
|
||||
fs.Var((*userVarValue)(&f.UserVars), "var", "specify a user variable")
|
||||
fs.Var((*AppendSliceValue)(&f.UserVarFiles), "var-file", "file with user variables")
|
||||
}
|
||||
|
||||
// userVarValue is a flag.Value that parses out user variables in
|
||||
// the form of 'key=value' and sets it on this map.
|
||||
type userVarValue map[string]string
|
||||
|
||||
func (v *userVarValue) String() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (v *userVarValue) Set(raw string) error {
|
||||
idx := strings.Index(raw, "=")
|
||||
if idx == -1 {
|
||||
return fmt.Errorf("No '=' value in arg: %s", raw)
|
||||
}
|
||||
|
||||
if *v == nil {
|
||||
*v = make(map[string]string)
|
||||
}
|
||||
|
||||
key, value := raw[0:idx], raw[idx+1:]
|
||||
(*v)[key] = value
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildOptionFlags(t *testing.T) {
|
||||
opts := new(BuildOptions)
|
||||
fs := flag.NewFlagSet("test", flag.ContinueOnError)
|
||||
BuildOptionFlags(fs, opts)
|
||||
|
||||
args := []string{
|
||||
"-except=foo,bar,baz",
|
||||
"-only=a,b",
|
||||
"-var=foo=bar",
|
||||
"-var", "bar=baz",
|
||||
"-var=foo=bang",
|
||||
"-var-file=foo",
|
||||
"-var-file=bar",
|
||||
}
|
||||
|
||||
err := fs.Parse(args)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar", "baz"}
|
||||
if !reflect.DeepEqual(opts.Except, expected) {
|
||||
t.Fatalf("bad: %#v", opts.Except)
|
||||
}
|
||||
|
||||
expected = []string{"a", "b"}
|
||||
if !reflect.DeepEqual(opts.Only, expected) {
|
||||
t.Fatalf("bad: %#v", opts.Only)
|
||||
}
|
||||
|
||||
if len(opts.UserVars) != 2 {
|
||||
t.Fatalf("bad: %#v", opts.UserVars)
|
||||
}
|
||||
|
||||
if opts.UserVars["foo"] != "bang" {
|
||||
t.Fatalf("bad: %#v", opts.UserVars)
|
||||
}
|
||||
|
||||
if opts.UserVars["bar"] != "baz" {
|
||||
t.Fatalf("bad: %#v", opts.UserVars)
|
||||
}
|
||||
|
||||
expected = []string{"foo", "bar"}
|
||||
if !reflect.DeepEqual(opts.UserVarFiles, expected) {
|
||||
t.Fatalf("bad: %#v", opts.UserVarFiles)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserVarValue_implements(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(userVarValue)
|
||||
if _, ok := raw.(flag.Value); !ok {
|
||||
t.Fatalf("userVarValue should be a Value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserVarValueSet(t *testing.T) {
|
||||
sv := new(userVarValue)
|
||||
err := sv.Set("key=value")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars := map[string]string(*sv)
|
||||
if vars["key"] != "value" {
|
||||
t.Fatalf("Bad: %#v", vars)
|
||||
}
|
||||
|
||||
// Empty value
|
||||
err = sv.Set("key=")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars = map[string]string(*sv)
|
||||
if vars["key"] != "" {
|
||||
t.Fatalf("Bad: %#v", vars)
|
||||
}
|
||||
|
||||
// Equal in value
|
||||
err = sv.Set("key=foo=bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
vars = map[string]string(*sv)
|
||||
if vars["key"] != "foo=bar" {
|
||||
t.Fatalf("Bad: %#v", vars)
|
||||
}
|
||||
|
||||
// No equal
|
||||
err = sv.Set("key")
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package command
|
||||
|
||||
import "strings"
|
||||
|
||||
// AppendSliceValue implements the flag.Value interface and allows multiple
|
||||
// calls to the same variable to append a list.
|
||||
type AppendSliceValue []string
|
||||
|
||||
func (s *AppendSliceValue) String() string {
|
||||
return strings.Join(*s, ",")
|
||||
}
|
||||
|
||||
func (s *AppendSliceValue) Set(value string) error {
|
||||
if *s == nil {
|
||||
*s = make([]string, 0, 1)
|
||||
}
|
||||
|
||||
*s = append(*s, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SliceValue implements the flag.Value interface and allows a list of
|
||||
// strings to be given on the command line and properly parsed into a slice
|
||||
// of strings internally.
|
||||
type SliceValue []string
|
||||
|
||||
func (s *SliceValue) String() string {
|
||||
return strings.Join(*s, ",")
|
||||
}
|
||||
|
||||
func (s *SliceValue) Set(value string) error {
|
||||
*s = strings.Split(value, ",")
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAppendSliceValue_implements(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(AppendSliceValue)
|
||||
if _, ok := raw.(flag.Value); !ok {
|
||||
t.Fatalf("AppendSliceValue should be a Value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendSliceValueSet(t *testing.T) {
|
||||
sv := new(AppendSliceValue)
|
||||
err := sv.Set("foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
err = sv.Set("bar")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar"}
|
||||
if !reflect.DeepEqual([]string(*sv), expected) {
|
||||
t.Fatalf("Bad: %#v", sv)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceValue_implements(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = new(SliceValue)
|
||||
if _, ok := raw.(flag.Value); !ok {
|
||||
t.Fatalf("SliceValue should be a Value")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSliceValueSet(t *testing.T) {
|
||||
sv := new(SliceValue)
|
||||
err := sv.Set("foo,bar,baz")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
expected := []string{"foo", "bar", "baz"}
|
||||
if !reflect.DeepEqual([]string(*sv), expected) {
|
||||
t.Fatalf("Bad: %#v", sv)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
jsonutil "github.com/mitchellh/packer/common/json"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
// BuildOptions is a set of options related to builds that can be set
|
||||
// from the command line.
|
||||
type BuildOptions struct {
|
||||
UserVarFiles []string
|
||||
UserVars map[string]string
|
||||
Except []string
|
||||
Only []string
|
||||
}
|
||||
|
||||
// Validate validates the options
|
||||
func (f *BuildOptions) Validate() error {
|
||||
if len(f.Except) > 0 && len(f.Only) > 0 {
|
||||
return errors.New("Only one of '-except' or '-only' may be specified.")
|
||||
}
|
||||
|
||||
if len(f.UserVarFiles) > 0 {
|
||||
for _, path := range f.UserVarFiles {
|
||||
if _, err := os.Stat(path); err != nil {
|
||||
return fmt.Errorf("Cannot access: %s", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AllUserVars returns the user variables, compiled from both the
|
||||
// file paths and the vars on the command line.
|
||||
func (f *BuildOptions) AllUserVars() (map[string]string, error) {
|
||||
all := make(map[string]string)
|
||||
|
||||
// Copy in the variables from the files
|
||||
for _, path := range f.UserVarFiles {
|
||||
fileVars, err := readFileVars(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for k, v := range fileVars {
|
||||
all[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Copy in the command-line vars
|
||||
for k, v := range f.UserVars {
|
||||
all[k] = v
|
||||
}
|
||||
|
||||
return all, nil
|
||||
}
|
||||
|
||||
// Builds returns the builds out of the given template that pass the
|
||||
// configured options.
|
||||
func (f *BuildOptions) Builds(t *packer.Template, cf *packer.ComponentFinder) ([]packer.Build, error) {
|
||||
buildNames := t.BuildNames()
|
||||
builds := make([]packer.Build, 0, len(buildNames))
|
||||
for _, buildName := range buildNames {
|
||||
if len(f.Except) > 0 {
|
||||
found := false
|
||||
for _, except := range f.Except {
|
||||
if buildName == except {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if found {
|
||||
log.Printf("Skipping build '%s' because specified by -except.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(f.Only) > 0 {
|
||||
found := false
|
||||
for _, only := range f.Only {
|
||||
if buildName == only {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
log.Printf("Skipping build '%s' because not specified by -only.", buildName)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Creating build: %s", buildName)
|
||||
build, err := t.Build(buildName, cf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Failed to create build '%s': \n\n%s", buildName, err)
|
||||
}
|
||||
|
||||
builds = append(builds, build)
|
||||
}
|
||||
|
||||
return builds, nil
|
||||
}
|
||||
|
||||
func readFileVars(path string) (map[string]string, error) {
|
||||
bytes, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
vars := make(map[string]string)
|
||||
err = jsonutil.Unmarshal(bytes, &vars)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return vars, nil
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildOptionsValidate(t *testing.T) {
|
||||
bf := new(BuildOptions)
|
||||
|
||||
err := bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Both set
|
||||
bf.Except = make([]string, 1)
|
||||
bf.Only = make([]string, 1)
|
||||
err = bf.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
|
||||
// One set
|
||||
bf.Except = make([]string, 1)
|
||||
bf.Only = make([]string, 0)
|
||||
err = bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
bf.Except = make([]string, 0)
|
||||
bf.Only = make([]string, 1)
|
||||
err = bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildOptionsValidate_userVarFiles(t *testing.T) {
|
||||
bf := new(BuildOptions)
|
||||
|
||||
err := bf.Validate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Non-existent file
|
||||
bf.UserVarFiles = []string{"ireallyshouldntexistanywhere"}
|
||||
err = bf.Validate()
|
||||
if err == nil {
|
||||
t.Fatal("should error")
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
@@ -63,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
|
||||
@@ -73,6 +83,17 @@ func DownloadableURL(original string) (string, error) {
|
||||
}
|
||||
|
||||
if url.Scheme == "file" {
|
||||
// For Windows absolute file paths, remove leading / prior to processing
|
||||
// since net/url turns "C:/" into "/C:/"
|
||||
if runtime.GOOS == "windows" && url.Path[0] == '/' {
|
||||
url.Path = url.Path[1:len(url.Path)]
|
||||
|
||||
// Also replace all backslashes with forwardslashes since Windows
|
||||
// users are likely to do this but the URL should actually only
|
||||
// contain forward slashes.
|
||||
url.Path = strings.Replace(url.Path, `\`, `/`, -1)
|
||||
}
|
||||
|
||||
if _, err := os.Stat(url.Path); err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// DownloadConfig is the configuration given to instantiate a new
|
||||
@@ -107,6 +108,11 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
var finalPath string
|
||||
if url.Scheme == "file" && !d.config.CopyFile {
|
||||
finalPath = url.Path
|
||||
|
||||
// Remove forward slash on absolute Windows file URLs before processing
|
||||
if runtime.GOOS == "windows" && finalPath[0] == '/' {
|
||||
finalPath = finalPath[1:len(finalPath)]
|
||||
}
|
||||
} else {
|
||||
finalPath = d.config.TargetPath
|
||||
|
||||
@@ -142,12 +148,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
|
||||
@@ -0,0 +1,40 @@
|
||||
package json
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Unmarshal is wrapper around json.Unmarshal that returns user-friendly
|
||||
// errors when there are syntax errors.
|
||||
func Unmarshal(data []byte, i interface{}) error {
|
||||
err := json.Unmarshal(data, i)
|
||||
if err != nil {
|
||||
syntaxErr, ok := err.(*json.SyntaxError)
|
||||
if !ok {
|
||||
return err
|
||||
}
|
||||
|
||||
// We have a syntax error. Extract out the line number and friends.
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/fizimmXtVfc
|
||||
newline := []byte{'\x0a'}
|
||||
|
||||
// Calculate the start/end position of the line where the error is
|
||||
start := bytes.LastIndex(data[:syntaxErr.Offset], newline) + 1
|
||||
end := len(data)
|
||||
if idx := bytes.Index(data[start:], newline); idx >= 0 {
|
||||
end = start + idx
|
||||
}
|
||||
|
||||
// Count the line number we're on plus the offset in the line
|
||||
line := bytes.Count(data[:start], newline) + 1
|
||||
pos := int(syntaxErr.Offset) - start - 1
|
||||
|
||||
err = fmt.Errorf("Error in line %d, char %d: %s\n%s",
|
||||
line, pos, syntaxErr, data[start:end])
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
package json
|
||||
@@ -0,0 +1,12 @@
|
||||
package common
|
||||
|
||||
// PackerConfig is a struct that contains the configuration keys that
|
||||
// are sent by packer, properly tagged already so mapstructure can load
|
||||
// them. Embed this structure into your configuration class to get it.
|
||||
type PackerConfig struct {
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
PackerBuilderType string `mapstructure:"packer_builder_type"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
PackerForce bool `mapstructure:"packer_force"`
|
||||
PackerUserVars map[string]string `mapstructure:"packer_user_variables"`
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -205,9 +205,14 @@ func (c *comm) Download(string, io.Writer) error {
|
||||
panic("not implemented yet")
|
||||
}
|
||||
|
||||
func (c *comm) newSession() (*ssh.Session, error) {
|
||||
func (c *comm) newSession() (session *ssh.Session, err error) {
|
||||
log.Println("opening new ssh session")
|
||||
session, err := c.client.NewSession()
|
||||
if c.client == nil {
|
||||
err = errors.New("client not available")
|
||||
} else {
|
||||
session, err = c.client.NewSession()
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
log.Printf("ssh session open error: '%s', attempting reconnect", err)
|
||||
if err := c.reconnect(); err != nil {
|
||||
@@ -225,6 +230,10 @@ func (c *comm) reconnect() (err error) {
|
||||
c.conn.Close()
|
||||
}
|
||||
|
||||
// Set the conn and client to nil since we'll recreate it
|
||||
c.conn = nil
|
||||
c.client = nil
|
||||
|
||||
log.Printf("reconnecting to TCP connection for SSH")
|
||||
c.conn, err = c.config.Connection()
|
||||
if err != nil {
|
||||
|
||||
@@ -10,6 +10,9 @@ import (
|
||||
"io"
|
||||
)
|
||||
|
||||
// SimpleKeychain makes it easy to use private keys in order to connect
|
||||
// via SSH, since the interface exposed by Go isn't the easiest to use
|
||||
// right away.
|
||||
type SimpleKeychain struct {
|
||||
keys []interface{}
|
||||
}
|
||||
@@ -26,6 +29,20 @@ func (k *SimpleKeychain) AddPEMKey(key string) (err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// AddPEMKeyPassword adds a PEM encoded private key that is protected by
|
||||
// a password to the keychain.
|
||||
func (k *SimpleKeychain) AddPEMKeyPassword(key string, password string) (err error) {
|
||||
block, _ := pem.Decode([]byte(key))
|
||||
bytes, _ := x509.DecryptPEMBlock(block, []byte(password))
|
||||
rsakey, err := x509.ParsePKCS1PrivateKey(bytes)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
k.keys = append(k.keys, rsakey)
|
||||
return
|
||||
}
|
||||
|
||||
// Key method for ssh.ClientKeyring interface
|
||||
func (k *SimpleKeychain) Key(i int) (interface{}, error) {
|
||||
if i < 0 || i >= len(k.keys) {
|
||||
|
||||
@@ -30,6 +30,7 @@ const defaultConfig = `
|
||||
"commands": {
|
||||
"build": "packer-command-build",
|
||||
"fix": "packer-command-fix",
|
||||
"inspect": "packer-command-inspect",
|
||||
"validate": "packer-command-validate"
|
||||
},
|
||||
|
||||
|
||||
@@ -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,19 +102,24 @@ 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)
|
||||
cache := &packer.FileCache{CacheDir: cacheDir}
|
||||
|
||||
// Determine if we're in machine-readable mode by mucking around with
|
||||
// the arguments...
|
||||
args, machineReadable := extractMachineReadable(os.Args[1:])
|
||||
|
||||
defer plugin.CleanupClients()
|
||||
|
||||
// Create the environment configuration
|
||||
envConfig := packer.DefaultEnvironmentConfig()
|
||||
envConfig.Cache = cache
|
||||
envConfig.Commands = config.CommandNames()
|
||||
@@ -86,23 +128,44 @@ func main() {
|
||||
envConfig.Components.Hook = config.LoadHook
|
||||
envConfig.Components.PostProcessor = config.LoadPostProcessor
|
||||
envConfig.Components.Provisioner = config.LoadProvisioner
|
||||
if machineReadable {
|
||||
envConfig.Ui = &packer.MachineReadableUi{
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
}
|
||||
|
||||
env, err := packer.NewEnvironment(envConfig)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Packer initialization error: \n\n%s\n", err)
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
setupSignalHandlers(env)
|
||||
|
||||
exitCode, err := env.Cli(os.Args[1:])
|
||||
exitCode, err := env.Cli(args)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
|
||||
os.Exit(1)
|
||||
return 1
|
||||
}
|
||||
|
||||
plugin.CleanupClients()
|
||||
os.Exit(exitCode)
|
||||
return exitCode
|
||||
}
|
||||
|
||||
// extractMachineReadable checks the args for the machine readable
|
||||
// flag and returns whether or not it is on. It modifies the args
|
||||
// to remove this flag.
|
||||
func extractMachineReadable(args []string) ([]string, bool) {
|
||||
for i, arg := range args {
|
||||
if arg == "-machine-readable" {
|
||||
// We found it. Slice it out.
|
||||
result := make([]string, len(args)-1)
|
||||
copy(result, args[:i])
|
||||
copy(result[i:], args[i+1:])
|
||||
return result, true
|
||||
}
|
||||
}
|
||||
|
||||
return args, false
|
||||
}
|
||||
|
||||
func loadConfig() (*config, error) {
|
||||
@@ -149,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
|
||||
}
|
||||
|
||||
+46
-16
@@ -23,6 +23,10 @@ const (
|
||||
// This is the key in configurations that is set to "true" when Packer
|
||||
// force build is enabled.
|
||||
ForceConfigKey = "packer_force"
|
||||
|
||||
// This key contains a map[string]string of the user variables for
|
||||
// template processing.
|
||||
UserVariablesConfigKey = "packer_user_variables"
|
||||
)
|
||||
|
||||
// A Build represents a single job within Packer that is responsible for
|
||||
@@ -35,7 +39,7 @@ type Build interface {
|
||||
|
||||
// Prepare configures the various components of this build and reports
|
||||
// any errors in doing so (such as syntax errors, validation errors, etc.)
|
||||
Prepare() error
|
||||
Prepare(v map[string]string) error
|
||||
|
||||
// Run runs the actual builder, returning an artifact implementation
|
||||
// of what is built. If anything goes wrong, an error is returned.
|
||||
@@ -73,6 +77,7 @@ type coreBuild struct {
|
||||
hooks map[string][]Hook
|
||||
postProcessors [][]coreBuildPostProcessor
|
||||
provisioners []coreBuildProvisioner
|
||||
variables map[string]string
|
||||
|
||||
debug bool
|
||||
force bool
|
||||
@@ -102,8 +107,9 @@ func (b *coreBuild) Name() string {
|
||||
}
|
||||
|
||||
// Prepare prepares the build by doing some initialization for the builder
|
||||
// and any hooks. This _must_ be called prior to Run.
|
||||
func (b *coreBuild) Prepare() (err error) {
|
||||
// and any hooks. This _must_ be called prior to Run. The parameter is the
|
||||
// overrides for the variables within the template (if any).
|
||||
func (b *coreBuild) Prepare(userVars map[string]string) (err error) {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
@@ -113,11 +119,37 @@ func (b *coreBuild) Prepare() (err error) {
|
||||
|
||||
b.prepareCalled = true
|
||||
|
||||
// Compile the variables
|
||||
variables := make(map[string]string)
|
||||
for k, v := range b.variables {
|
||||
variables[k] = v
|
||||
}
|
||||
|
||||
if userVars != nil {
|
||||
errs := make([]error, 0)
|
||||
for k, v := range userVars {
|
||||
if _, ok := variables[k]; !ok {
|
||||
errs = append(
|
||||
errs, fmt.Errorf("Unknown user variable: %s", k))
|
||||
continue
|
||||
}
|
||||
|
||||
variables[k] = v
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &MultiError{
|
||||
Errors: errs,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
BuildNameConfigKey: b.name,
|
||||
BuilderTypeConfigKey: b.builderType,
|
||||
DebugConfigKey: b.debug,
|
||||
ForceConfigKey: b.force,
|
||||
BuildNameConfigKey: b.name,
|
||||
BuilderTypeConfigKey: b.builderType,
|
||||
DebugConfigKey: b.debug,
|
||||
ForceConfigKey: b.force,
|
||||
UserVariablesConfigKey: variables,
|
||||
}
|
||||
|
||||
// Prepare the builder
|
||||
@@ -181,11 +213,10 @@ func (b *coreBuild) Run(originalUi Ui, cache Cache) ([]Artifact, error) {
|
||||
hook := &DispatchHook{hooks}
|
||||
artifacts := make([]Artifact, 0, 1)
|
||||
|
||||
// The builder just has a normal Ui, but prefixed
|
||||
builderUi := &PrefixedUi{
|
||||
fmt.Sprintf("==> %s", b.Name()),
|
||||
fmt.Sprintf(" %s", b.Name()),
|
||||
originalUi,
|
||||
// The builder just has a normal Ui, but targetted
|
||||
builderUi := &TargettedUi{
|
||||
Target: b.Name(),
|
||||
Ui: originalUi,
|
||||
}
|
||||
|
||||
log.Printf("Running builder: %s", b.builderType)
|
||||
@@ -208,10 +239,9 @@ PostProcessorRunSeqLoop:
|
||||
for _, ppSeq := range b.postProcessors {
|
||||
priorArtifact := builderArtifact
|
||||
for i, corePP := range ppSeq {
|
||||
ppUi := &PrefixedUi{
|
||||
fmt.Sprintf("==> %s (%s)", b.Name(), corePP.processorType),
|
||||
fmt.Sprintf(" %s (%s)", b.Name(), corePP.processorType),
|
||||
originalUi,
|
||||
ppUi := &TargettedUi{
|
||||
Target: fmt.Sprintf("%s (%s)", b.Name(), corePP.processorType),
|
||||
Ui: originalUi,
|
||||
}
|
||||
|
||||
builderUi.Say(fmt.Sprintf("Running post-processor: %s", corePP.processorType))
|
||||
|
||||
+81
-23
@@ -23,6 +23,7 @@ func testBuild() *coreBuild {
|
||||
coreBuildPostProcessor{&TestPostProcessor{artifactId: "pp"}, "testPP", 42, true},
|
||||
},
|
||||
},
|
||||
variables: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +31,15 @@ func testBuilder() *TestBuilder {
|
||||
return &TestBuilder{}
|
||||
}
|
||||
|
||||
func testDefaultPackerConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
BuildNameConfigKey: "test",
|
||||
BuilderTypeConfigKey: "foo",
|
||||
DebugConfigKey: false,
|
||||
ForceConfigKey: false,
|
||||
UserVariablesConfigKey: make(map[string]string),
|
||||
}
|
||||
}
|
||||
func TestBuild_Name(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -39,18 +49,12 @@ func TestBuild_Name(t *testing.T) {
|
||||
|
||||
func TestBuild_Prepare(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
BuildNameConfigKey: "test",
|
||||
BuilderTypeConfigKey: "foo",
|
||||
DebugConfigKey: false,
|
||||
ForceConfigKey: false,
|
||||
}
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
|
||||
build := testBuild()
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
assert.True(builder.prepareCalled, "prepare should be called")
|
||||
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
|
||||
|
||||
@@ -67,7 +71,7 @@ func TestBuild_Prepare(t *testing.T) {
|
||||
|
||||
func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
build := testBuild()
|
||||
if err := build.Prepare(); err != nil {
|
||||
if err := build.Prepare(nil); err != nil {
|
||||
t.Fatalf("bad error: %s", err)
|
||||
}
|
||||
|
||||
@@ -82,24 +86,20 @@ func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
}
|
||||
}()
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
}
|
||||
|
||||
func TestBuild_Prepare_Debug(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
packerConfig := map[string]interface{}{
|
||||
BuildNameConfigKey: "test",
|
||||
BuilderTypeConfigKey: "foo",
|
||||
DebugConfigKey: true,
|
||||
ForceConfigKey: false,
|
||||
}
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
packerConfig[DebugConfigKey] = true
|
||||
|
||||
build := testBuild()
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
build.SetDebug(true)
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
assert.True(builder.prepareCalled, "prepare should be called")
|
||||
assert.Equal(builder.prepareConfig, []interface{}{42, packerConfig}, "prepare config should be 42")
|
||||
|
||||
@@ -109,6 +109,64 @@ func TestBuild_Prepare_Debug(t *testing.T) {
|
||||
assert.Equal(prov.prepConfigs, []interface{}{42, packerConfig}, "prepare should be called with proper config")
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_default(t *testing.T) {
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
packerConfig[UserVariablesConfigKey] = map[string]string{
|
||||
"foo": "bar",
|
||||
}
|
||||
|
||||
build := testBuild()
|
||||
build.variables["foo"] = "bar"
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
err := build.Prepare(nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !builder.prepareCalled {
|
||||
t.Fatal("prepare should be called")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(builder.prepareConfig[1], packerConfig) {
|
||||
t.Fatalf("prepare bad: %#v", builder.prepareConfig[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_nonexist(t *testing.T) {
|
||||
build := testBuild()
|
||||
build.variables["foo"] = "bar"
|
||||
|
||||
err := build.Prepare(map[string]string{"bar": "baz"})
|
||||
if err == nil {
|
||||
t.Fatal("should have had error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrepare_variables_override(t *testing.T) {
|
||||
packerConfig := testDefaultPackerConfig()
|
||||
packerConfig[UserVariablesConfigKey] = map[string]string{
|
||||
"foo": "baz",
|
||||
}
|
||||
|
||||
build := testBuild()
|
||||
build.variables["foo"] = "bar"
|
||||
builder := build.builder.(*TestBuilder)
|
||||
|
||||
err := build.Prepare(map[string]string{"foo": "baz"})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if !builder.prepareCalled {
|
||||
t.Fatal("prepare should be called")
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(builder.prepareConfig[1], packerConfig) {
|
||||
t.Fatalf("prepare bad: %#v", builder.prepareConfig[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuild_Run(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
@@ -116,7 +174,7 @@ func TestBuild_Run(t *testing.T) {
|
||||
ui := testUi()
|
||||
|
||||
build := testBuild()
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err := build.Run(ui, cache)
|
||||
assert.Nil(err, "should not error")
|
||||
assert.Equal(len(artifacts), 2, "should have two artifacts")
|
||||
@@ -152,7 +210,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
build := testBuild()
|
||||
build.postProcessors = [][]coreBuildPostProcessor{}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err := build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -177,7 +235,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -205,7 +263,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -235,7 +293,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
@@ -262,7 +320,7 @@ func TestBuild_Run_Artifacts(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
build.Prepare()
|
||||
build.Prepare(nil)
|
||||
artifacts, err = build.Run(ui, cache)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
|
||||
+26
-2
@@ -72,6 +72,14 @@ func (r *RemoteCmd) StartWithUi(c Communicator, ui Ui) error {
|
||||
defer stdout_w.Close()
|
||||
defer stderr_w.Close()
|
||||
|
||||
// Retain the original stdout/stderr that we can replace back in.
|
||||
originalStdout := r.Stdout
|
||||
originalStderr := r.Stderr
|
||||
defer func() {
|
||||
r.Stdout = originalStdout
|
||||
r.Stderr = originalStderr
|
||||
}()
|
||||
|
||||
// Set the writers for the output so that we get it streamed to us
|
||||
if r.Stdout == nil {
|
||||
r.Stdout = stdout_w
|
||||
@@ -108,9 +116,9 @@ OutputLoop:
|
||||
for {
|
||||
select {
|
||||
case output := <-stderrCh:
|
||||
ui.Message(strings.TrimSpace(output))
|
||||
ui.Message(r.cleanOutputLine(output))
|
||||
case output := <-stdoutCh:
|
||||
ui.Message(strings.TrimSpace(output))
|
||||
ui.Message(r.cleanOutputLine(output))
|
||||
case <-exitCh:
|
||||
break OutputLoop
|
||||
}
|
||||
@@ -156,3 +164,19 @@ func (r *RemoteCmd) Wait() {
|
||||
|
||||
<-r.exitCh
|
||||
}
|
||||
|
||||
// cleanOutputLine cleans up a line so that '\r' don't muck up the
|
||||
// UI output when we're reading from a remote command.
|
||||
func (r *RemoteCmd) cleanOutputLine(line string) string {
|
||||
// Trim surrounding whitespace
|
||||
line = strings.TrimSpace(line)
|
||||
|
||||
// Trim up to the first carriage return, since that text would be
|
||||
// lost anyways.
|
||||
idx := strings.LastIndex(line, "\r")
|
||||
if idx > -1 {
|
||||
line = line[idx+1:]
|
||||
}
|
||||
|
||||
return line
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestRemoteCmd_StartWithUi(t *testing.T) {
|
||||
Stdout: rcOutput,
|
||||
}
|
||||
|
||||
testUi := &ReaderWriterUi{
|
||||
testUi := &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: uiOutput,
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 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 ConfigTemplate struct {
|
||||
UserVars map[string]string
|
||||
|
||||
root *template.Template
|
||||
i int
|
||||
}
|
||||
|
||||
// NewConfigTemplate creates a new configuration template processor.
|
||||
func NewConfigTemplate() (*ConfigTemplate, error) {
|
||||
result := &ConfigTemplate{
|
||||
UserVars: make(map[string]string),
|
||||
}
|
||||
|
||||
result.root = template.New("configTemplateRoot")
|
||||
result.root.Funcs(template.FuncMap{
|
||||
"timestamp": templateTimestamp,
|
||||
"user": result.templateUser,
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Process processes a single string, compiling and executing the template.
|
||||
func (t *ConfigTemplate) Process(s string, data interface{}) (string, error) {
|
||||
tpl, err := t.root.New(t.nextTemplateName()).Parse(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err := tpl.Execute(buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
// Validate the template.
|
||||
func (t *ConfigTemplate) Validate(s string) error {
|
||||
root, err := t.root.Clone()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = root.New("template").Parse(s)
|
||||
return err
|
||||
}
|
||||
|
||||
func (t *ConfigTemplate) nextTemplateName() string {
|
||||
name := fmt.Sprintf("tpl%d", t.i)
|
||||
t.i++
|
||||
return name
|
||||
}
|
||||
|
||||
// User is the function exposed as "user" within the templates and
|
||||
// looks up user variables.
|
||||
func (t *ConfigTemplate) templateUser(n string) (string, error) {
|
||||
result, ok := t.UserVars[n]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("uknown user var: %s", n)
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func templateTimestamp() string {
|
||||
return strconv.FormatInt(time.Now().UTC().Unix(), 10)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"math"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestConfigTemplateProcess_timestamp(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
result, err := tpl.Process(`{{timestamp}}`, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
val, err := strconv.ParseInt(result, 10, 64)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
currentTime := time.Now().UTC().Unix()
|
||||
if math.Abs(float64(currentTime-val)) > 10 {
|
||||
t.Fatalf("val: %d (current: %d)", val, currentTime)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplateProcess_user(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
tpl.UserVars["foo"] = "bar"
|
||||
|
||||
result, err := tpl.Process(`{{user "foo"}}`, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if result != "bar" {
|
||||
t.Fatalf("bad: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigTemplateValidate(t *testing.T) {
|
||||
tpl, err := NewConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Valid
|
||||
err = tpl.Validate(`{{user "foo"}}`)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
// Invalid
|
||||
err = tpl.Validate(`{{idontexist}}`)
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
}
|
||||
+69
-19
@@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// The function type used to lookup Builder implementations.
|
||||
@@ -68,12 +69,18 @@ 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 {
|
||||
config := &EnvironmentConfig{}
|
||||
config.Commands = make([]string, 0)
|
||||
config.Ui = &ReaderWriterUi{
|
||||
config.Ui = &BasicUi{
|
||||
Reader: os.Stdin,
|
||||
Writer: os.Stdout,
|
||||
}
|
||||
@@ -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,39 +273,82 @@ 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
|
||||
}
|
||||
|
||||
// Pad the key with spaces so that they're all the same width
|
||||
key = fmt.Sprintf("%v%v", key, strings.Repeat(" ", maxKeyLen-len(key)))
|
||||
for _, message := range entries {
|
||||
if message != "" {
|
||||
e.ui.Say(message)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// 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.")
|
||||
}
|
||||
|
||||
// Returns the UI for the environment. The UI is the interface that should
|
||||
|
||||
@@ -17,9 +17,20 @@ 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 = &ReaderWriterUi{
|
||||
config.Ui = &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
@@ -45,8 +56,8 @@ func TestEnvironment_DefaultConfig_Ui(t *testing.T) {
|
||||
config := DefaultEnvironmentConfig()
|
||||
assert.NotNil(config.Ui, "default UI should not be nil")
|
||||
|
||||
rwUi, ok := config.Ui.(*ReaderWriterUi)
|
||||
assert.True(ok, "default UI should be ReaderWriterUi")
|
||||
rwUi, ok := config.Ui.(*BasicUi)
|
||||
assert.True(ok, "default UI should be BasicUi")
|
||||
assert.Equal(rwUi.Writer, os.Stdout, "default UI should go to stdout")
|
||||
assert.Equal(rwUi.Reader, os.Stdin, "default UI should read from stdin")
|
||||
}
|
||||
@@ -175,7 +186,7 @@ func TestEnvironment_DefaultCli_Help(t *testing.T) {
|
||||
|
||||
// A little lambda to help us test the output actually contains help
|
||||
testOutput := func() {
|
||||
buffer := defaultEnv.Ui().(*ReaderWriterUi).Writer.(*bytes.Buffer)
|
||||
buffer := defaultEnv.Ui().(*BasicUi).Writer.(*bytes.Buffer)
|
||||
output := buffer.String()
|
||||
buffer.Reset()
|
||||
assert.True(strings.Contains(output, "usage: packer"), "should print help")
|
||||
@@ -341,7 +352,7 @@ func TestEnvironmentProvisioner_Error(t *testing.T) {
|
||||
func TestEnvironment_SettingUi(t *testing.T) {
|
||||
assert := asserts.NewTestingAsserts(t, true)
|
||||
|
||||
ui := &ReaderWriterUi{
|
||||
ui := &BasicUi{
|
||||
Reader: new(bytes.Buffer),
|
||||
Writer: new(bytes.Buffer),
|
||||
}
|
||||
|
||||
+4
-4
@@ -30,8 +30,8 @@ func (b *build) Name() (result string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (b *build) Prepare() (err error) {
|
||||
if cerr := b.client.Call("Build.Prepare", new(interface{}), &err); cerr != nil {
|
||||
func (b *build) Prepare(v map[string]string) (err error) {
|
||||
if cerr := b.client.Call("Build.Prepare", v, &err); cerr != nil {
|
||||
return cerr
|
||||
}
|
||||
|
||||
@@ -86,8 +86,8 @@ func (b *BuildServer) Name(args *interface{}, reply *string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *BuildServer) Prepare(args interface{}, reply *error) error {
|
||||
*reply = b.build.Prepare()
|
||||
func (b *BuildServer) Prepare(v map[string]string, reply *error) error {
|
||||
*reply = b.build.Prepare(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ var testBuildArtifact = &testArtifact{}
|
||||
type testBuild struct {
|
||||
nameCalled bool
|
||||
prepareCalled bool
|
||||
prepareVars map[string]string
|
||||
runCalled bool
|
||||
runCache packer.Cache
|
||||
runUi packer.Ui
|
||||
@@ -28,8 +29,9 @@ func (b *testBuild) Name() string {
|
||||
return "name"
|
||||
}
|
||||
|
||||
func (b *testBuild) Prepare() error {
|
||||
func (b *testBuild) Prepare(v map[string]string) error {
|
||||
b.prepareCalled = true
|
||||
b.prepareVars = v
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -78,8 +80,15 @@ func TestBuildRPC(t *testing.T) {
|
||||
assert.True(b.nameCalled, "name should be called")
|
||||
|
||||
// Test Prepare
|
||||
bClient.Prepare()
|
||||
bClient.Prepare(map[string]string{"foo": "bar"})
|
||||
assert.True(b.prepareCalled, "prepare should be called")
|
||||
if len(b.prepareVars) != 1 {
|
||||
t.Fatalf("bad vars: %#v", b.prepareVars)
|
||||
}
|
||||
|
||||
if b.prepareVars["foo"] != "bar" {
|
||||
t.Fatalf("bad vars: %#v", b.prepareVars)
|
||||
}
|
||||
|
||||
// Test Run
|
||||
cache := new(testCache)
|
||||
|
||||
@@ -4,6 +4,7 @@ import "encoding/gob"
|
||||
|
||||
func init() {
|
||||
gob.Register(new(map[string]interface{}))
|
||||
gob.Register(new(map[string]string))
|
||||
gob.Register(make([]interface{}, 0))
|
||||
gob.Register(new(BasicError))
|
||||
}
|
||||
|
||||
@@ -17,6 +17,12 @@ type UiServer struct {
|
||||
ui packer.Ui
|
||||
}
|
||||
|
||||
// The arguments sent to Ui.Machine
|
||||
type UiMachineArgs struct {
|
||||
Category string
|
||||
Args []string
|
||||
}
|
||||
|
||||
func (u *Ui) Ask(query string) (result string, err error) {
|
||||
err = u.client.Call("Ui.Ask", query, &result)
|
||||
return
|
||||
@@ -28,6 +34,17 @@ func (u *Ui) Error(message string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ui) Machine(t string, args ...string) {
|
||||
rpcArgs := &UiMachineArgs{
|
||||
Category: t,
|
||||
Args: args,
|
||||
}
|
||||
|
||||
if err := u.client.Call("Ui.Machine", rpcArgs, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (u *Ui) Message(message string) {
|
||||
if err := u.client.Call("Ui.Message", message, new(interface{})); err != nil {
|
||||
panic(err)
|
||||
@@ -52,6 +69,13 @@ func (u *UiServer) Error(message *string, reply *interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UiServer) Machine(args *UiMachineArgs, reply *interface{}) error {
|
||||
u.ui.Machine(args.Category, args.Args...)
|
||||
|
||||
*reply = nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *UiServer) Message(message *string, reply *interface{}) error {
|
||||
u.ui.Message(*message)
|
||||
*reply = nil
|
||||
|
||||
@@ -3,6 +3,7 @@ package rpc
|
||||
import (
|
||||
"cgl.tideland.biz/asserts"
|
||||
"net/rpc"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -11,6 +12,9 @@ type testUi struct {
|
||||
askQuery string
|
||||
errorCalled bool
|
||||
errorMessage string
|
||||
machineCalled bool
|
||||
machineType string
|
||||
machineArgs []string
|
||||
messageCalled bool
|
||||
messageMessage string
|
||||
sayCalled bool
|
||||
@@ -28,6 +32,12 @@ func (u *testUi) Error(message string) {
|
||||
u.errorMessage = message
|
||||
}
|
||||
|
||||
func (u *testUi) Machine(t string, args ...string) {
|
||||
u.machineCalled = true
|
||||
u.machineType = t
|
||||
u.machineArgs = args
|
||||
}
|
||||
|
||||
func (u *testUi) Message(message string) {
|
||||
u.messageCalled = true
|
||||
u.messageMessage = message
|
||||
@@ -72,4 +82,18 @@ func TestUiRPC(t *testing.T) {
|
||||
|
||||
uiClient.Say("message")
|
||||
assert.Equal(ui.sayMessage, "message", "message should be correct")
|
||||
|
||||
uiClient.Machine("foo", "bar", "baz")
|
||||
if !ui.machineCalled {
|
||||
t.Fatal("machine should be called")
|
||||
}
|
||||
|
||||
if ui.machineType != "foo" {
|
||||
t.Fatalf("bad type: %#v", ui.machineType)
|
||||
}
|
||||
|
||||
expected := []string{"bar", "baz"}
|
||||
if !reflect.DeepEqual(ui.machineArgs, expected) {
|
||||
t.Fatalf("bad: %#v", ui.machineArgs)
|
||||
}
|
||||
}
|
||||
|
||||
+62
-48
@@ -2,9 +2,12 @@ package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
jsonutil "github.com/mitchellh/packer/common/json"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
)
|
||||
|
||||
@@ -13,6 +16,7 @@ import (
|
||||
// "interface{}" pointers since we actually don't know what their contents
|
||||
// are until we read the "type" field.
|
||||
type rawTemplate struct {
|
||||
Variables map[string]string
|
||||
Builders []map[string]interface{}
|
||||
Hooks map[string][]string
|
||||
Provisioners []map[string]interface{}
|
||||
@@ -22,40 +26,41 @@ type rawTemplate struct {
|
||||
// The Template struct represents a parsed template, parsed into the most
|
||||
// completed form it can be without additional processing by the caller.
|
||||
type Template struct {
|
||||
Builders map[string]rawBuilderConfig
|
||||
Variables map[string]string
|
||||
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 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
|
||||
@@ -65,31 +70,8 @@ type rawProvisionerConfig struct {
|
||||
// way.
|
||||
func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
var rawTplInterface interface{}
|
||||
err = json.Unmarshal(data, &rawTplInterface)
|
||||
err = jsonutil.Unmarshal(data, &rawTplInterface)
|
||||
if err != nil {
|
||||
syntaxErr, ok := err.(*json.SyntaxError)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
// We have a syntax error. Extract out the line number and friends.
|
||||
// https://groups.google.com/forum/#!topic/golang-nuts/fizimmXtVfc
|
||||
newline := []byte{'\x0a'}
|
||||
|
||||
// Calculate the start/end position of the line where the error is
|
||||
start := bytes.LastIndex(data[:syntaxErr.Offset], newline) + 1
|
||||
end := len(data)
|
||||
if idx := bytes.Index(data[start:], newline); idx >= 0 {
|
||||
end = start + idx
|
||||
}
|
||||
|
||||
// Count the line number we're on plus the offset in the line
|
||||
line := bytes.Count(data[:start], newline) + 1
|
||||
pos := int(syntaxErr.Offset) - start - 1
|
||||
|
||||
err = fmt.Errorf("Error in line %d, char %d: %s\n%s",
|
||||
line, pos, syntaxErr, data[start:end])
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -123,14 +105,20 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
}
|
||||
|
||||
t = &Template{}
|
||||
t.Builders = make(map[string]rawBuilderConfig)
|
||||
t.Variables = make(map[string]string)
|
||||
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 {
|
||||
t.Variables[k] = v
|
||||
}
|
||||
|
||||
// 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 {
|
||||
@@ -165,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
|
||||
}
|
||||
@@ -180,7 +168,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
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]
|
||||
@@ -201,7 +189,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
continue
|
||||
}
|
||||
|
||||
config.rawConfig = pp
|
||||
config.RawConfig = pp
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,7 +218,7 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
// actively reject them as invalid configuration.
|
||||
delete(v, "override")
|
||||
|
||||
raw.rawConfig = v
|
||||
raw.RawConfig = v
|
||||
}
|
||||
|
||||
if len(t.Builders) == 0 {
|
||||
@@ -247,6 +235,31 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// ParseTemplateFile takes the given template file and parses it into
|
||||
// a single template.
|
||||
func ParseTemplateFile(path string) (*Template, error) {
|
||||
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) {
|
||||
switch v := rawV.(type) {
|
||||
case string:
|
||||
@@ -369,7 +382,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,
|
||||
}
|
||||
}
|
||||
@@ -392,7 +405,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 {
|
||||
@@ -407,11 +420,12 @@ 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,
|
||||
provisioners: provisioners,
|
||||
variables: t.Variables,
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user