Compare commits
109 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3adc5e03d7 | |||
| ef1ec3708b | |||
| ee1ad5fa56 | |||
| 65d74f90c1 | |||
| 27bada1ba9 | |||
| f7587be617 | |||
| 62430fdcea | |||
| b25bed634f | |||
| 8c01d2c226 | |||
| f59dfa90a9 | |||
| eb009ca4e3 | |||
| abc2bc60ab | |||
| 308205cc6f | |||
| cd6e154bd1 | |||
| 4ecb92358b | |||
| c1ad88aaa6 | |||
| ee9aa9b071 | |||
| 8d5c2091d4 | |||
| baa539d413 | |||
| 70155fc695 | |||
| c46aae5b09 | |||
| d5731a13d4 | |||
| 248f18da37 | |||
| 62199c8c73 | |||
| 324a2229a6 | |||
| 8b25acfab6 | |||
| f5003c4e19 | |||
| 534c923a44 | |||
| da415312ee | |||
| c0cdec92db | |||
| 04c4d7f7a9 | |||
| 6d4cf32982 | |||
| fb0af12c22 | |||
| c7ce4bf74c | |||
| 8735ac99db | |||
| 3068d0e3fd | |||
| 30cb058c81 | |||
| cf191350cd | |||
| f1c8f0a8cd | |||
| 49dc2c4cba | |||
| 4f2344f48b | |||
| 7f9c755a42 | |||
| 6d6d8795df | |||
| 05c7d08bb2 | |||
| c259f30e58 | |||
| ab574af78c | |||
| e8d31bc21a | |||
| 94c0fac5ad | |||
| 73154286fc | |||
| d891e578e2 | |||
| d7dccaeea2 | |||
| df560dbbe1 | |||
| 259986e7e5 | |||
| 40e9f9a76f | |||
| 2f698cca77 | |||
| 7247a69581 | |||
| 2958b5d9c2 | |||
| a8da773e40 | |||
| 663cce5aaa | |||
| 82a3aba0d3 | |||
| 2e15bed7b6 | |||
| 9264f76053 | |||
| ae279a8b61 | |||
| f31f5ed759 | |||
| 8ce8171ec1 | |||
| b4b9469d3c | |||
| 925734d06b | |||
| 2e88ddc965 | |||
| b729ba0e00 | |||
| 4bb5f8a6aa | |||
| 2fc795d299 | |||
| f931b28851 | |||
| cf13f57528 | |||
| aff4389fc8 | |||
| 4d4dd85ef4 | |||
| be40fb7abb | |||
| 00a5ca32ae | |||
| 93e5082d04 | |||
| f36972f52a | |||
| 8ece9a60d5 | |||
| 432cab3fcf | |||
| 586a9a7c32 | |||
| 359ba01c6a | |||
| dbbbc38af6 | |||
| f2111b6d12 | |||
| f27ea933a4 | |||
| 4aa55c40de | |||
| 06108b4f96 | |||
| 0e3b1e59a8 | |||
| 29fa621907 | |||
| d4cd9352d6 | |||
| 4de76ccd3f | |||
| ac5c90bd7e | |||
| 7bff7f8a22 | |||
| 126f2bc07f | |||
| 0b1b76a4db | |||
| 9ca3d05241 | |||
| 2a7a7173f1 | |||
| 8e231f9264 | |||
| 3f9df2992c | |||
| 400faa57a5 | |||
| fc78bf3dd6 | |||
| 8cf21324dc | |||
| 974ac26c9c | |||
| b6884da2a1 | |||
| 9f17257f19 | |||
| 5bf33a0e91 | |||
| 4b2b23c32f | |||
| 9c9c60aabf |
@@ -1,3 +1,61 @@
|
||||
## 0.1.5 (July 7, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* "file" uploader will upload files from the machine running Packer to the
|
||||
remote machine.
|
||||
* VirtualBox guest additions URL and checksum can now be specified, allowing
|
||||
the VirtualBox builder to have the ability to be used completely offline.
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: If SCP is not available, a more descriptive error message
|
||||
is shown telling the user. [GH-127]
|
||||
* shell: Scripts are now executed by default according to their shebang,
|
||||
not with `/bin/sh`. [GH-105]
|
||||
* shell: You can specify what interpreter you want inline scripts to
|
||||
run with `inline_shebang`.
|
||||
* virtualbox: Delete the packer-made SSH port forwarding prior to
|
||||
exporting the VM.
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* core: Non-200 response codes on downloads now show proper errors.
|
||||
[GH-141]
|
||||
* amazon-ebs: SSH handshake is retried. [GH-130]
|
||||
* vagrant: The `BuildName` template propery works properly in
|
||||
the output path.
|
||||
* vagrant: Properly configure the provider-specific post-processors so
|
||||
things like `vagrantfile_template` work. [GH-129]
|
||||
* vagrant: Close filehandles when copying files so Windows can
|
||||
rename files. [GH-100]
|
||||
|
||||
## 0.1.4 (July 2, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
* virtualbox: Can now be built headless with the "Headless" option. [GH-99]
|
||||
* virtualbox: <wait5> and <wait10> codes for waiting 5 and 10 seconds
|
||||
during the boot sequence, respectively. [GH-97]
|
||||
* vmware: Can now be built headless with the "Headless" option. [GH-99]
|
||||
* vmware: <wait5> and <wait10> codes for waiting 5 and 10 seconds
|
||||
during the boot sequence, respectively. [GH-97]
|
||||
* vmware: Disks are defragmented and compacted at the end of the build.
|
||||
This can be disabled using "skip_compaction"
|
||||
|
||||
IMPROVEMENTS:
|
||||
|
||||
* core: Template syntax errors now show line and character number. [GH-56]
|
||||
* amazon-ebs: Access key and secret access key default to
|
||||
environmental variables. [GH-40]
|
||||
* virtualbox: Send password for keyboard-interactive auth [GH-121]
|
||||
* vmware: Send password for keyboard-interactive auth [GH-121]
|
||||
|
||||
BUG FIXES:
|
||||
|
||||
* vmware: Wait until shut down cleans up properly to avoid corrupt
|
||||
disk files [GH-111]
|
||||
|
||||
## 0.1.3 (July 1, 2013)
|
||||
|
||||
FEATURES:
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
# Contributing to Packer
|
||||
|
||||
**First:** if you're unsure or afraid of _anything_, just ask
|
||||
or submit the issue or pull request anyways. You won't be yelled at for
|
||||
giving your best effort. The worst that can happen is that you'll be
|
||||
politely asked to change something. We appreciate any sort of contributions,
|
||||
and don't want a wall of rules to get in the way of that.
|
||||
|
||||
However, for those individuals who want a bit more guidance on the
|
||||
best way to contribute to the project, read on. This document will cover
|
||||
what we're looking for. By addressing all the points we're looking for,
|
||||
it raises the chances we can quickly merge or address your contributions.
|
||||
|
||||
## Issues
|
||||
|
||||
### Reporting an Issue
|
||||
|
||||
* Make sure you test against the latest released version. It is possible
|
||||
we already fixed the bug you're experiencing.
|
||||
|
||||
* Provide a reproducible test case. If a contributor can't reproduce an
|
||||
issue, then it dramatically lowers the chances it'll get fixed. And in
|
||||
some cases, the issue will eventually be closed.
|
||||
|
||||
* Respond promptly to any questions made by the Packer team to your issue.
|
||||
Stale issues will be closed.
|
||||
|
||||
### Issue Lifecycle
|
||||
|
||||
1. The issue is reported.
|
||||
|
||||
2. The issue is verified and categorized by a Packer collaborator.
|
||||
Categorization is done via tags. For example, bugs are marked as "bugs"
|
||||
and easy fixes are marked as "easy".
|
||||
|
||||
3. Unless it is critical, the issue is left for a period of time (sometimes
|
||||
many weeks), giving outside contributors a chance to address the issue.
|
||||
|
||||
4. The issue is addressed in a pull request or commit. The issue will be
|
||||
referenced in the commit message so that the code that fixes it is clearly
|
||||
linked.
|
||||
|
||||
5. The issue is closed.
|
||||
@@ -15,8 +15,7 @@ format:
|
||||
|
||||
test:
|
||||
@echo "$(OK_COLOR)==> Testing Packer...$(NO_COLOR)"
|
||||
@go list -f '{{range .TestImports}}{{.}}\
|
||||
{{end}}' ./... | xargs -n1 go get -d
|
||||
@go list -f '{{range .TestImports}}{{.}} {{end}}' ./... | xargs -n1 go get -d
|
||||
go test ./...
|
||||
|
||||
.PHONY: all format test
|
||||
|
||||
@@ -77,8 +77,8 @@ For some additional dependencies, Go needs [Mercurial](http://mercurial.selenic.
|
||||
to be installed. Packer itself doesn't require this but a dependency of a
|
||||
dependency does.
|
||||
|
||||
Next, clone this repository then just type `make`. In a few moments,
|
||||
you'll have a working `packer` executable:
|
||||
Next, clone this repository into `$GOPATH/src/github.com/mitchellh/packer` and
|
||||
then just type `make`. In a few moments, you'll have a working `packer` executable:
|
||||
|
||||
```
|
||||
$ make
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/mitchellh/packer/builder/common"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"os"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
@@ -57,6 +58,22 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.AccessKey == "" {
|
||||
b.config.AccessKey = os.Getenv("AWS_ACCESS_KEY_ID")
|
||||
}
|
||||
|
||||
if b.config.AccessKey == "" {
|
||||
b.config.AccessKey = os.Getenv("AWS_ACCESS_KEY")
|
||||
}
|
||||
|
||||
if b.config.SecretKey == "" {
|
||||
b.config.SecretKey = os.Getenv("AWS_SECRET_ACCESS_KEY")
|
||||
}
|
||||
|
||||
if b.config.SecretKey == "" {
|
||||
b.config.SecretKey = os.Getenv("AWS_SECRET_KEY")
|
||||
}
|
||||
|
||||
if b.config.SSHPort == 0 {
|
||||
b.config.SSHPort = 22
|
||||
}
|
||||
|
||||
@@ -2,9 +2,19 @@ package amazonebs
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Clear out the AWS access key env vars so they don't
|
||||
// affect our tests.
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||
os.Setenv("AWS_ACCESS_KEY", "")
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||
os.Setenv("AWS_SECRET_KEY", "")
|
||||
}
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"access_key": "foo",
|
||||
@@ -59,6 +69,31 @@ func TestBuilderPrepare_AccessKey(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test env
|
||||
delete(config, "access_key")
|
||||
os.Setenv("AWS_ACCESS_KEY_ID", "foo")
|
||||
defer os.Setenv("AWS_ACCESS_KEY_ID", "")
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.AccessKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.AccessKey)
|
||||
}
|
||||
|
||||
delete(config, "access_key")
|
||||
os.Setenv("AWS_ACCESS_KEY", "foo")
|
||||
defer os.Setenv("AWS_ACCESS_KEY", "")
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.AccessKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.AccessKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_AMIName(t *testing.T) {
|
||||
@@ -167,6 +202,31 @@ func TestBuilderPrepare_SecretKey(t *testing.T) {
|
||||
if err == nil {
|
||||
t.Fatal("should have error")
|
||||
}
|
||||
|
||||
// Test env
|
||||
delete(config, "secret_key")
|
||||
os.Setenv("AWS_SECRET_ACCESS_KEY", "foo")
|
||||
defer os.Setenv("AWS_SECRET_ACCESS_KEY", "")
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SecretKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.SecretKey)
|
||||
}
|
||||
|
||||
delete(config, "secret_key")
|
||||
os.Setenv("AWS_SECRET_KEY", "foo")
|
||||
defer os.Setenv("AWS_SECRET_KEY", "")
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.SecretKey != "foo" {
|
||||
t.Errorf("access key invalid: %s", b.config.SecretKey)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_SourceAmi(t *testing.T) {
|
||||
|
||||
@@ -14,10 +14,64 @@ import (
|
||||
)
|
||||
|
||||
type stepConnectSSH struct {
|
||||
conn net.Conn
|
||||
cancel bool
|
||||
conn net.Conn
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(config)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
var comm packer.Communicator
|
||||
var err error
|
||||
|
||||
waitDone := make(chan bool, 1)
|
||||
go func() {
|
||||
comm, err = s.waitForSSH(state)
|
||||
waitDone <- true
|
||||
}()
|
||||
|
||||
log.Printf("Waiting for SSH, up to timeout: %s", config.SSHTimeout.String())
|
||||
|
||||
timeout := time.After(config.SSHTimeout)
|
||||
WaitLoop:
|
||||
for {
|
||||
// Wait for either SSH to become available, a timeout to occur,
|
||||
// or an interrupt to come through.
|
||||
select {
|
||||
case <-waitDone:
|
||||
if err != nil {
|
||||
ui.Error(fmt.Sprintf("Error waiting for SSH: %s", err))
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state["communicator"] = comm
|
||||
break WaitLoop
|
||||
case <-timeout:
|
||||
ui.Error("Timeout waiting for SSH.")
|
||||
s.cancel = true
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
log.Println("Interrupt detected, quitting waiting for SSH.")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
s.conn = nil
|
||||
}
|
||||
}
|
||||
|
||||
// This blocks until SSH becomes available, and sends the communicator
|
||||
// on the given channel.
|
||||
func (s *stepConnectSSH) waitForSSH(state map[string]interface{}) (packer.Communicator, error) {
|
||||
config := state["config"].(config)
|
||||
instance := state["instance"].(*ec2.Instance)
|
||||
privateKey := state["privateKey"].(string)
|
||||
@@ -28,98 +82,70 @@ func (s *stepConnectSSH) Run(state map[string]interface{}) multistep.StepAction
|
||||
keyring := &ssh.SimpleKeychain{}
|
||||
err := keyring.AddPEMKey(privateKey)
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Build the actual SSH client configuration
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}
|
||||
|
||||
// Start trying to connect to SSH
|
||||
connected := make(chan bool, 1)
|
||||
connectQuit := make(chan bool, 1)
|
||||
defer func() {
|
||||
connectQuit <- true
|
||||
}()
|
||||
|
||||
go func() {
|
||||
var err error
|
||||
|
||||
ui.Say("Connecting to the instance via SSH...")
|
||||
attempts := 0
|
||||
for {
|
||||
select {
|
||||
case <-connectQuit:
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
attempts += 1
|
||||
log.Printf(
|
||||
"Opening TCP conn for SSH to %s:%d (attempt %d)",
|
||||
instance.DNSName, config.SSHPort, attempts)
|
||||
s.conn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort))
|
||||
if err == nil {
|
||||
break
|
||||
}
|
||||
|
||||
// A brief sleep so we're not being overly zealous attempting
|
||||
// to connect to the instance.
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
connected <- true
|
||||
}()
|
||||
|
||||
log.Printf("Waiting up to %s for SSH connection", config.SSHTimeout)
|
||||
timeout := time.After(config.SSHTimeout)
|
||||
|
||||
ConnectWaitLoop:
|
||||
for {
|
||||
select {
|
||||
case <-connected:
|
||||
// We connected. Just break the loop.
|
||||
break ConnectWaitLoop
|
||||
case <-timeout:
|
||||
err := errors.New("Timeout waiting for SSH to become available.")
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
case <-time.After(1 * time.Second):
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
log.Println("Interrupt detected, quitting waiting for SSH.")
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("Error setting up SSH config: %s", err)
|
||||
}
|
||||
|
||||
ui.Say("Waiting for SSH to become available...")
|
||||
var comm packer.Communicator
|
||||
if err == nil {
|
||||
comm, err = ssh.New(s.conn, sshConfig)
|
||||
var nc net.Conn
|
||||
for {
|
||||
if nc != nil {
|
||||
nc.Close()
|
||||
}
|
||||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
if s.cancel {
|
||||
log.Println("SSH wait cancelled. Exiting loop.")
|
||||
return nil, errors.New("SSH wait cancelled")
|
||||
}
|
||||
|
||||
// Attempt to connect to SSH port
|
||||
log.Printf(
|
||||
"Opening TCP conn for SSH to %s:%d",
|
||||
instance.DNSName, config.SSHPort)
|
||||
nc, err := net.Dial("tcp",
|
||||
fmt.Sprintf("%s:%d", instance.DNSName, config.SSHPort))
|
||||
if err != nil {
|
||||
log.Printf("TCP connection to SSH ip/port failed: %s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// Build the actual SSH client configuration
|
||||
sshConfig := &gossh.ClientConfig{
|
||||
User: config.SSHUsername,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthKeyring(keyring),
|
||||
},
|
||||
}
|
||||
|
||||
sshConnectSuccess := make(chan bool, 1)
|
||||
go func() {
|
||||
comm, err = ssh.New(nc, sshConfig)
|
||||
if err != nil {
|
||||
log.Printf("SSH connection fail: %s", err)
|
||||
sshConnectSuccess <- false
|
||||
return
|
||||
}
|
||||
|
||||
sshConnectSuccess <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case success := <-sshConnectSuccess:
|
||||
if !success {
|
||||
continue
|
||||
}
|
||||
case <-time.After(5 * time.Second):
|
||||
log.Printf("SSH handshake timeout. Trying again.")
|
||||
continue
|
||||
}
|
||||
|
||||
ui.Say("Connected via SSH!")
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err := fmt.Errorf("Error connecting to SSH: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Set the communicator on the state bag so it can be used later
|
||||
state["communicator"] = comm
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (s *stepConnectSSH) Cleanup(map[string]interface{}) {
|
||||
if s.conn != nil {
|
||||
s.conn.Close()
|
||||
}
|
||||
// Store the connection so we can close it later
|
||||
s.conn = nc
|
||||
return comm, nil
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ func (d *DownloadClient) Get() (string, error) {
|
||||
|
||||
log.Printf("Downloading: %s", url.String())
|
||||
err = d.downloader.Download(f, url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
if d.config.Hash != nil {
|
||||
@@ -160,11 +163,22 @@ func (*HTTPDownloader) Cancel() {
|
||||
}
|
||||
|
||||
func (d *HTTPDownloader) Download(dst io.Writer, src *url.URL) error {
|
||||
log.Printf("Starting download: %s", src.String())
|
||||
resp, err := http.Get(src.String())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
log.Printf(
|
||||
"Non-200 status code: %d. Getting error body.", resp.StatusCode)
|
||||
|
||||
errorBody := new(bytes.Buffer)
|
||||
io.Copy(errorBody, resp.Body)
|
||||
return fmt.Errorf("HTTP error '%d'! Remote side responded:\n%s",
|
||||
resp.StatusCode, errorBody.String())
|
||||
}
|
||||
|
||||
d.progress = 0
|
||||
d.total = uint(resp.ContentLength)
|
||||
|
||||
|
||||
@@ -25,28 +25,31 @@ type Builder struct {
|
||||
}
|
||||
|
||||
type config struct {
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
BootWait time.Duration ``
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
ISOMD5 string `mapstructure:"iso_md5"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
ShutdownTimeout time.Duration ``
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHWaitTimeout time.Duration ``
|
||||
VBoxVersionFile string `mapstructure:"virtualbox_version_file"`
|
||||
VBoxManage [][]string `mapstructure:"vboxmanage"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
BootWait time.Duration ``
|
||||
DiskSize uint `mapstructure:"disk_size"`
|
||||
GuestAdditionsPath string `mapstructure:"guest_additions_path"`
|
||||
GuestAdditionsURL string `mapstructure:"guest_additions_url"`
|
||||
GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
|
||||
GuestOSType string `mapstructure:"guest_os_type"`
|
||||
Headless bool `mapstructure:"headless"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
ISOMD5 string `mapstructure:"iso_md5"`
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
ShutdownTimeout time.Duration ``
|
||||
SSHHostPortMin uint `mapstructure:"ssh_host_port_min"`
|
||||
SSHHostPortMax uint `mapstructure:"ssh_host_port_max"`
|
||||
SSHPassword string `mapstructure:"ssh_password"`
|
||||
SSHPort uint `mapstructure:"ssh_port"`
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
SSHWaitTimeout time.Duration ``
|
||||
VBoxVersionFile string `mapstructure:"virtualbox_version_file"`
|
||||
VBoxManage [][]string `mapstructure:"vboxmanage"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
PackerDebug bool `mapstructure:"packer_debug"`
|
||||
@@ -169,6 +172,47 @@ func (b *Builder) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsSHA256 != "" {
|
||||
b.config.GuestAdditionsSHA256 = strings.ToLower(b.config.GuestAdditionsSHA256)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsURL != "" {
|
||||
url, err := url.Parse(b.config.GuestAdditionsURL)
|
||||
if err != nil {
|
||||
errs = append(errs, fmt.Errorf("guest_additions_url is not a valid URL: %s", err))
|
||||
} else {
|
||||
if url.Scheme == "" {
|
||||
url.Scheme = "file"
|
||||
}
|
||||
|
||||
if url.Scheme == "file" {
|
||||
if _, err := os.Stat(url.Path); err != nil {
|
||||
errs = append(errs, fmt.Errorf("guest_additions_url points to bad file: %s", err))
|
||||
}
|
||||
} else {
|
||||
supportedSchemes := []string{"file", "http", "https"}
|
||||
scheme := strings.ToLower(url.Scheme)
|
||||
|
||||
found := false
|
||||
for _, supported := range supportedSchemes {
|
||||
if scheme == supported {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
errs = append(errs, fmt.Errorf("Unsupported URL scheme in guest_additions_url: %s", scheme))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) == 0 {
|
||||
// Put the URL back together since we may have modified it
|
||||
b.config.GuestAdditionsURL = url.String()
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(b.config.OutputDir); err == nil {
|
||||
errs = append(errs, errors.New("Output directory already exists. It must not exist."))
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "disk_size")
|
||||
delete(config, "guest_additions_path")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
@@ -141,6 +141,81 @@ func TestBuilderPrepare_GuestAdditionsPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_GuestAdditionsSHA256(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "guest_additions_sha256")
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("bad err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsSHA256 != "" {
|
||||
t.Fatalf("bad: %s", b.config.GuestAdditionsSHA256)
|
||||
}
|
||||
|
||||
config["guest_additions_sha256"] = "FOO"
|
||||
b = Builder{}
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsSHA256 != "foo" {
|
||||
t.Fatalf("bad size: %s", b.config.GuestAdditionsSHA256)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_GuestAdditionsURL(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
config["guest_additions_url"] = ""
|
||||
err := b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsURL != "" {
|
||||
t.Fatalf("should be empty: %s", b.config.GuestAdditionsURL)
|
||||
}
|
||||
|
||||
config["guest_additions_url"] = "i/am/a/file/that/doesnt/exist"
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Error("should have error")
|
||||
}
|
||||
|
||||
config["guest_additions_url"] = "file:i/am/a/file/that/doesnt/exist"
|
||||
err = b.Prepare(config)
|
||||
if err == nil {
|
||||
t.Error("should have error")
|
||||
}
|
||||
|
||||
config["guest_additions_url"] = "http://www.packer.io"
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Errorf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
config["guest_additions_url"] = tf.Name()
|
||||
err = b.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if b.config.GuestAdditionsURL != "file://"+tf.Name() {
|
||||
t.Fatalf("guest_additions_url should be modified: %s", b.config.GuestAdditionsURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuilderPrepare_HTTPPort(t *testing.T) {
|
||||
var b Builder
|
||||
config := testConfig()
|
||||
|
||||
@@ -22,7 +22,10 @@ func (s *stepCreateVM) Run(state map[string]interface{}) multistep.StepAction {
|
||||
name := config.VMName
|
||||
|
||||
commands := make([][]string, 4)
|
||||
commands[0] = []string{"createvm", "--name", name, "--ostype", config.GuestOSType, "--register"}
|
||||
commands[0] = []string{
|
||||
"createvm", "--name", name,
|
||||
"--ostype", config.GuestOSType, "--register",
|
||||
}
|
||||
commands[1] = []string{
|
||||
"modifyvm", name,
|
||||
"--boot1", "disk", "--boot2", "dvd", "--boot3", "none", "--boot4", "none",
|
||||
|
||||
@@ -33,7 +33,9 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep
|
||||
cache := state["cache"].(packer.Cache)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
config := state["config"].(*config)
|
||||
|
||||
// Get VBox version
|
||||
version, err := driver.Version()
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error reading version for guest additions download: %s", err)
|
||||
@@ -45,68 +47,18 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep
|
||||
version = newVersion
|
||||
}
|
||||
|
||||
// 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", version)
|
||||
checksumsFile, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf(
|
||||
"Failed creating temporary file to store guest addition checksums: %s",
|
||||
err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
checksumsFile.Close()
|
||||
defer os.Remove(checksumsFile.Name())
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: checksumsUrl,
|
||||
TargetPath: checksumsFile.Name(),
|
||||
Hash: nil,
|
||||
}
|
||||
|
||||
log.Printf("Downloading guest addition checksums: %s", checksumsUrl)
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
checksumsPath, action := s.progressDownload(download, state)
|
||||
if action != multistep.ActionContinue {
|
||||
return action
|
||||
}
|
||||
|
||||
additionsName := fmt.Sprintf("VBoxGuestAdditions_%s.iso", version)
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
defer checksumsF.Close()
|
||||
// Use provided version or get it from virtualbox.org
|
||||
var checksum string
|
||||
|
||||
// We copy the contents of the file into memory. In general this file
|
||||
// is quite small so that is okay. In the future, we probably want to
|
||||
// use bufio and iterate line by line.
|
||||
var contents bytes.Buffer
|
||||
io.Copy(&contents, checksumsF)
|
||||
|
||||
checksum := ""
|
||||
for _, line := range strings.Split(contents.String(), "\n") {
|
||||
parts := strings.Fields(line)
|
||||
log.Printf("Checksum file parts: %#v", parts)
|
||||
if len(parts) != 2 {
|
||||
// Bogus line
|
||||
continue
|
||||
if config.GuestAdditionsSHA256 != "" {
|
||||
checksum = config.GuestAdditionsSHA256
|
||||
} else {
|
||||
checksum, action = s.downloadAdditionsSHA256(state, version, additionsName)
|
||||
if action != multistep.ActionContinue {
|
||||
return action
|
||||
}
|
||||
|
||||
if strings.HasSuffix(parts[1], additionsName) {
|
||||
checksum = parts[0]
|
||||
log.Printf("Guest additions checksum: %s", checksum)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if checksum == "" {
|
||||
state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
checksumBytes, err := hex.DecodeString(checksum)
|
||||
@@ -115,23 +67,29 @@ func (s *stepDownloadGuestAdditions) Run(state map[string]interface{}) multistep
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
url := fmt.Sprintf(
|
||||
"http://download.virtualbox.org/virtualbox/%s/%s",
|
||||
version, additionsName)
|
||||
// Use the provided source (URL or file path) or generate it
|
||||
url := config.GuestAdditionsURL
|
||||
if url == "" {
|
||||
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{
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: url,
|
||||
TargetPath: cachePath,
|
||||
Hash: sha256.New(),
|
||||
Checksum: checksumBytes,
|
||||
}
|
||||
|
||||
download = common.NewDownloadClient(downloadConfig)
|
||||
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
|
||||
@@ -179,3 +137,72 @@ DownloadWaitLoop:
|
||||
|
||||
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)
|
||||
|
||||
checksumsFile, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf(
|
||||
"Failed creating temporary file to store guest addition checksums: %s",
|
||||
err)
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
defer os.Remove(checksumsFile.Name())
|
||||
|
||||
checksumsFile.Close()
|
||||
|
||||
downloadConfig := &common.DownloadConfig{
|
||||
Url: checksumsUrl,
|
||||
TargetPath: checksumsFile.Name(),
|
||||
Hash: nil,
|
||||
}
|
||||
|
||||
log.Printf("Downloading guest addition checksums: %s", checksumsUrl)
|
||||
download := common.NewDownloadClient(downloadConfig)
|
||||
checksumsPath, action := s.progressDownload(download, state)
|
||||
if action != multistep.ActionContinue {
|
||||
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)
|
||||
if err != nil {
|
||||
state["error"] = fmt.Errorf("Error opening guest addition checksums: %s", err)
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
defer checksumsF.Close()
|
||||
|
||||
// We copy the contents of the file into memory. In general this file
|
||||
// is quite small so that is okay. In the future, we probably want to
|
||||
// use bufio and iterate line by line.
|
||||
var contents bytes.Buffer
|
||||
io.Copy(&contents, checksumsF)
|
||||
|
||||
checksum := ""
|
||||
for _, line := range strings.Split(contents.String(), "\n") {
|
||||
parts := strings.Fields(line)
|
||||
log.Printf("Checksum file parts: %#v", parts)
|
||||
if len(parts) != 2 {
|
||||
// Bogus line
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasSuffix(parts[1], additionsName) {
|
||||
checksum = parts[0]
|
||||
log.Printf("Guest additions checksum: %s", checksum)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if checksum == "" {
|
||||
state["error"] = fmt.Errorf("The checksum for the file '%s' could not be found.", additionsName)
|
||||
return "", multistep.ActionHalt
|
||||
}
|
||||
|
||||
return checksum, multistep.ActionContinue
|
||||
|
||||
}
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// This step creates the virtual disk that will be used as the
|
||||
// hard drive for the virtual machine.
|
||||
// This step cleans up forwarded ports and exports the VM to an OVF.
|
||||
//
|
||||
// Uses:
|
||||
//
|
||||
@@ -22,9 +21,20 @@ func (s *stepExport) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
vmName := state["vmName"].(string)
|
||||
|
||||
// Clear out the Packer-created forwarding rule
|
||||
ui.Say(fmt.Sprintf("Deleting forwarded port mapping for SSH (host port %d)", state["sshHostPort"]))
|
||||
command := []string{"modifyvm", vmName, "--natpf1", "delete", "packerssh"}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error deleting port forwarding rule: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
// Export the VM to an OVF
|
||||
outputPath := filepath.Join(config.OutputDir, "packer.ovf")
|
||||
|
||||
command := []string{
|
||||
command = []string{
|
||||
"export",
|
||||
vmName,
|
||||
"--output",
|
||||
|
||||
@@ -36,7 +36,7 @@ func (s *stepForwardSSH) Run(state map[string]interface{}) multistep.StepAction
|
||||
}
|
||||
}
|
||||
|
||||
// Attach the disk to the controller
|
||||
// Create a forwarded port mapping to the VM
|
||||
ui.Say(fmt.Sprintf("Creating forwarded port mapping for SSH (host port %d)", sshHostPort))
|
||||
command := []string{
|
||||
"modifyvm", vmName,
|
||||
|
||||
@@ -23,7 +23,14 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
|
||||
vmName := state["vmName"].(string)
|
||||
|
||||
ui.Say("Starting the virtual machine...")
|
||||
command := []string{"startvm", vmName, "--type", "gui"}
|
||||
guiArgument := "gui"
|
||||
if config.Headless == true {
|
||||
ui.Message("WARNING: The VM will be started in headless mode, as configured.\n" +
|
||||
"In headless mode, errors during the boot sequence or OS setup\n" +
|
||||
"won't be easily visible. Use at your own discretion.")
|
||||
guiArgument = "headless"
|
||||
}
|
||||
command := []string{"startvm", vmName, "--type", guiArgument}
|
||||
if err := driver.VBoxManage(command...); err != nil {
|
||||
err := fmt.Errorf("Error starting VM: %s", err)
|
||||
state["error"] = err
|
||||
|
||||
@@ -59,6 +59,16 @@ func (s *stepTypeBootCommand) Run(state map[string]interface{}) multistep.StepAc
|
||||
continue
|
||||
}
|
||||
|
||||
if code == "wait5" {
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
if code == "wait10" {
|
||||
time.Sleep(10 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// Since typing is sometimes so slow, we check for an interrupt
|
||||
// in between each character.
|
||||
if _, ok := state[multistep.StateCancelled]; ok {
|
||||
@@ -116,11 +126,23 @@ func scancodes(message string) []string {
|
||||
var scancode []string
|
||||
|
||||
if strings.HasPrefix(message, "<wait>") {
|
||||
log.Printf("Special code <wait> found, will sleep at this point.")
|
||||
log.Printf("Special code <wait> found, will sleep 1 second at this point.")
|
||||
scancode = []string{"wait"}
|
||||
message = message[len("<wait>"):]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<wait5>") {
|
||||
log.Printf("Special code <wait5> found, will sleep 5 seconds at this point.")
|
||||
scancode = []string{"wait5"}
|
||||
message = message[len("<wait5>"):]
|
||||
}
|
||||
|
||||
if strings.HasPrefix(message, "<wait10>") {
|
||||
log.Printf("Special code <wait10> found, will sleep 10 seconds at this point.")
|
||||
scancode = []string{"wait10"}
|
||||
message = message[len("<wait10>"):]
|
||||
}
|
||||
|
||||
if scancode == nil {
|
||||
for specialCode, specialValue := range special {
|
||||
if strings.HasPrefix(message, specialCode) {
|
||||
|
||||
@@ -112,6 +112,8 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
|
||||
User: config.SSHUser,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||
gossh.ClientAuthKeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -33,11 +33,13 @@ type config struct {
|
||||
ISOUrl string `mapstructure:"iso_url"`
|
||||
VMName string `mapstructure:"vm_name"`
|
||||
OutputDir string `mapstructure:"output_directory"`
|
||||
Headless bool `mapstructure:"headless"`
|
||||
HTTPDir string `mapstructure:"http_directory"`
|
||||
HTTPPortMin uint `mapstructure:"http_port_min"`
|
||||
HTTPPortMax uint `mapstructure:"http_port_max"`
|
||||
BootCommand []string `mapstructure:"boot_command"`
|
||||
BootWait time.Duration ``
|
||||
SkipCompaction bool `mapstructure:"skip_compaction"`
|
||||
ShutdownCommand string `mapstructure:"shutdown_command"`
|
||||
ShutdownTimeout time.Duration ``
|
||||
SSHUser string `mapstructure:"ssh_username"`
|
||||
@@ -239,6 +241,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
|
||||
&stepProvision{},
|
||||
&stepShutdown{},
|
||||
&stepCleanFiles{},
|
||||
&stepCompactDisk{},
|
||||
}
|
||||
|
||||
// Setup the state bag
|
||||
|
||||
@@ -12,6 +12,9 @@ import (
|
||||
|
||||
// A driver is able to talk to VMware, control virtual machines, etc.
|
||||
type Driver interface {
|
||||
// CompactDisk compacts a virtual disk.
|
||||
CompactDisk(string) error
|
||||
|
||||
// CreateDisk creates a virtual disk with the given size.
|
||||
CreateDisk(string, string) error
|
||||
|
||||
@@ -19,7 +22,7 @@ type Driver interface {
|
||||
IsRunning(string) (bool, error)
|
||||
|
||||
// Start starts a VM specified by the path to the VMX given.
|
||||
Start(string) error
|
||||
Start(string, bool) error
|
||||
|
||||
// Stop stops a VM specified by the path to the VMX given.
|
||||
Stop(string) error
|
||||
@@ -40,6 +43,20 @@ type Fusion5Driver struct {
|
||||
AppPath string
|
||||
}
|
||||
|
||||
func (d *Fusion5Driver) CompactDisk(diskPath string) error {
|
||||
defragCmd := exec.Command(d.vdiskManagerPath(), "-d", diskPath)
|
||||
if _, _, err := d.runAndLog(defragCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
shrinkCmd := exec.Command(d.vdiskManagerPath(), "-k", diskPath)
|
||||
if _, _, err := d.runAndLog(shrinkCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *Fusion5Driver) CreateDisk(output string, size string) error {
|
||||
cmd := exec.Command(d.vdiskManagerPath(), "-c", "-s", size, "-a", "lsilogic", "-t", "1", output)
|
||||
if _, _, err := d.runAndLog(cmd); err != nil {
|
||||
@@ -70,8 +87,13 @@ func (d *Fusion5Driver) IsRunning(vmxPath string) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (d *Fusion5Driver) Start(vmxPath string) error {
|
||||
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, "gui")
|
||||
func (d *Fusion5Driver) Start(vmxPath string, headless bool) error {
|
||||
guiArgument := "gui"
|
||||
if headless == true {
|
||||
guiArgument = "nogui"
|
||||
}
|
||||
|
||||
cmd := exec.Command(d.vmrunPath(), "-T", "fusion", "start", vmxPath, guiArgument)
|
||||
if _, _, err := d.runAndLog(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package vmware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
)
|
||||
|
||||
// This step compacts the virtual disk for the VM unless the "skip_compaction"
|
||||
// boolean is true.
|
||||
//
|
||||
// Uses:
|
||||
// config *config
|
||||
// driver Driver
|
||||
// full_disk_path string
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
// <nothing>
|
||||
type stepCompactDisk struct{}
|
||||
|
||||
func (stepCompactDisk) Run(state map[string]interface{}) multistep.StepAction {
|
||||
config := state["config"].(*config)
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
full_disk_path := state["full_disk_path"].(string)
|
||||
|
||||
if config.SkipCompaction == true {
|
||||
log.Println("Skipping disk compaction step...")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
ui.Say("Compacting the disk image")
|
||||
if err := driver.CompactDisk(full_disk_path); err != nil {
|
||||
state["error"] = fmt.Errorf("Error compacting disk: %s", err)
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
func (stepCompactDisk) Cleanup(map[string]interface{}) {}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
// ui packer.Ui
|
||||
//
|
||||
// Produces:
|
||||
// <nothing>
|
||||
// full_disk_path (string) - The full path to the created disk.
|
||||
type stepCreateDisk struct{}
|
||||
|
||||
func (stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction {
|
||||
@@ -24,14 +24,16 @@ func (stepCreateDisk) Run(state map[string]interface{}) multistep.StepAction {
|
||||
ui := state["ui"].(packer.Ui)
|
||||
|
||||
ui.Say("Creating virtual machine disk")
|
||||
output := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
|
||||
if err := driver.CreateDisk(output, fmt.Sprintf("%dM", config.DiskSize)); err != nil {
|
||||
full_disk_path := filepath.Join(config.OutputDir, config.DiskName+".vmdk")
|
||||
if err := driver.CreateDisk(full_disk_path, fmt.Sprintf("%dM", config.DiskSize)); err != nil {
|
||||
err := fmt.Errorf("Error creating disk: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
return multistep.ActionHalt
|
||||
}
|
||||
|
||||
state["full_disk_path"] = full_disk_path
|
||||
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
|
||||
@@ -27,13 +27,21 @@ func (s *stepRun) Run(state map[string]interface{}) multistep.StepAction {
|
||||
driver := state["driver"].(Driver)
|
||||
ui := state["ui"].(packer.Ui)
|
||||
vmxPath := state["vmx_path"].(string)
|
||||
vncPort := state["vnc_port"].(uint)
|
||||
|
||||
// Set the VMX path so that we know we started the machine
|
||||
s.bootTime = time.Now()
|
||||
s.vmxPath = vmxPath
|
||||
|
||||
ui.Say("Starting virtual machine...")
|
||||
if err := driver.Start(vmxPath); err != nil {
|
||||
if config.Headless {
|
||||
ui.Message(fmt.Sprintf(
|
||||
"The VM will be run headless, without a GUI. If you want to\n"+
|
||||
"view the screen of the VM, connect via VNC without a password to\n"+
|
||||
"127.0.0.1:%d", vncPort))
|
||||
}
|
||||
|
||||
if err := driver.Start(vmxPath, config.Headless); err != nil {
|
||||
err := fmt.Errorf("Error starting VM: %s", err)
|
||||
state["error"] = err
|
||||
ui.Error(err.Error())
|
||||
|
||||
@@ -6,6 +6,8 @@ import (
|
||||
"github.com/mitchellh/multistep"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -72,6 +74,34 @@ func (s *stepShutdown) Run(state map[string]interface{}) multistep.StepAction {
|
||||
}
|
||||
}
|
||||
|
||||
ui.Message("Waiting for VMware to clean up after itself...")
|
||||
lockPattern := filepath.Join(config.OutputDir, "*.lck")
|
||||
timer := time.After(15 * time.Second)
|
||||
LockWaitLoop:
|
||||
for {
|
||||
locks, err := filepath.Glob(lockPattern)
|
||||
if err == nil {
|
||||
if len(locks) == 0 {
|
||||
log.Println("No more lock files found. VMware is clean.")
|
||||
break
|
||||
}
|
||||
|
||||
if len(locks) == 1 && strings.HasSuffix(locks[0], ".vmx.lck") {
|
||||
log.Println("Only waiting on VMX lock. VMware is clean.")
|
||||
break
|
||||
}
|
||||
|
||||
log.Printf("Waiting on lock files: %#v", locks)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-timer:
|
||||
log.Println("Reached timeout on waiting for clean VMware. Assuming clean.")
|
||||
break LockWaitLoop
|
||||
case <-time.After(1 * time.Second):
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("VM shut down.")
|
||||
return multistep.ActionContinue
|
||||
}
|
||||
|
||||
@@ -114,6 +114,20 @@ func vncSendString(c *vnc.ClientConn, original string) {
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<wait5>") {
|
||||
log.Printf("Special code '<wait5>' found, sleeping 5 seconds")
|
||||
time.Sleep(5 * time.Second)
|
||||
original = original[len("<wait5>"):]
|
||||
continue
|
||||
}
|
||||
|
||||
if strings.HasPrefix(original, "<wait10>") {
|
||||
log.Printf("Special code '<wait10>' found, sleeping 10 seconds")
|
||||
time.Sleep(10 * time.Second)
|
||||
original = original[len("<wait10>"):]
|
||||
continue
|
||||
}
|
||||
|
||||
for specialCode, specialValue := range special {
|
||||
if strings.HasPrefix(original, specialCode) {
|
||||
log.Printf("Special code '%s' found, replacing with: %d", specialCode, specialValue)
|
||||
|
||||
@@ -157,6 +157,8 @@ func (s *stepWaitForSSH) waitForSSH(state map[string]interface{}) (packer.Commun
|
||||
User: config.SSHUser,
|
||||
Auth: []gossh.ClientAuth{
|
||||
gossh.ClientAuthPassword(ssh.Password(config.SSHPassword)),
|
||||
gossh.ClientAuthKeyboardInteractive(
|
||||
ssh.PasswordKeyboardInteractive(config.SSHPassword)),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package build
|
||||
|
||||
const helpText = `
|
||||
Usage: packer build TEMPLATE
|
||||
Usage: packer build [options] TEMPLATE
|
||||
|
||||
Will execute multiple builds in parallel as defined in the template.
|
||||
The various artifacts created by the template will be outputted.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package validate
|
||||
|
||||
const helpString = `
|
||||
Usage: packer validate TEMPLATE
|
||||
Usage: packer validate [options] TEMPLATE
|
||||
|
||||
Checks the template is valid by parsing the template and also
|
||||
checking the configuration with the various builders, provisioners, etc.
|
||||
|
||||
@@ -3,6 +3,7 @@ package ssh
|
||||
import (
|
||||
"bytes"
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
@@ -145,6 +146,14 @@ func (c *comm) Upload(path string, input io.Reader) error {
|
||||
// Otherwise, we have an ExitErorr, meaning we can just read
|
||||
// the exit status
|
||||
log.Printf("non-zero exit status: %d", exitErr.ExitStatus())
|
||||
|
||||
// If we exited with status 127, it means SCP isn't available.
|
||||
// Return a more descriptive error for that.
|
||||
if exitErr.ExitStatus() == 127 {
|
||||
return errors.New(
|
||||
"SCP failed to start. This usually means that SCP is not\n" +
|
||||
"properly installed on the remote system.")
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package ssh
|
||||
|
||||
import "log"
|
||||
|
||||
// An implementation of ssh.ClientPassword so that you can use a static
|
||||
// string password for the password to ClientAuthPassword.
|
||||
type Password string
|
||||
@@ -7,3 +9,24 @@ type Password string
|
||||
func (p Password) Password(user string) (string, error) {
|
||||
return string(p), nil
|
||||
}
|
||||
|
||||
// An implementation of ssh.ClientKeyboardInteractive that simply sends
|
||||
// back the password for all questions. The questions are logged.
|
||||
type PasswordKeyboardInteractive string
|
||||
|
||||
func (p PasswordKeyboardInteractive) Challenge(user, instruction string, questions []string, echos []bool) ([]string, error) {
|
||||
log.Printf("Keyboard interactive challenge: ")
|
||||
log.Printf("-- User: %s", user)
|
||||
log.Printf("-- Instructions: %s", instruction)
|
||||
for i, question := range questions {
|
||||
log.Printf("-- Question %d: %s", i+1, question)
|
||||
}
|
||||
|
||||
// Just send the password back for all questions
|
||||
answers := make([]string, len(questions))
|
||||
for i, _ := range answers {
|
||||
answers[i] = string(p)
|
||||
}
|
||||
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package ssh
|
||||
|
||||
import (
|
||||
"code.google.com/p/go.crypto/ssh"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -24,3 +25,23 @@ func TestPasswordPassword(t *testing.T) {
|
||||
t.Fatalf("invalid password: %s", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordKeyboardInteractive_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = PasswordKeyboardInteractive("foo")
|
||||
if _, ok := raw.(ssh.ClientKeyboardInteractive); !ok {
|
||||
t.Fatal("PasswordKeyboardInteractive must implement ClientKeyboardInteractive")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordKeybardInteractive_Challenge(t *testing.T) {
|
||||
p := PasswordKeyboardInteractive("foo")
|
||||
result, err := p.Challenge("foo", "bar", []string{"one", "two"}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("err not nil: %s", err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(result, []string{"foo", "foo"}) {
|
||||
t.Fatalf("invalid password: %#v", result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ const defaultConfig = `
|
||||
},
|
||||
|
||||
"provisioners": {
|
||||
"file": "packer-provisioner-file",
|
||||
"shell": "packer-provisioner-shell"
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -121,7 +121,8 @@ func (b *coreBuild) Prepare() (err error) {
|
||||
// Prepare the post-processors
|
||||
for _, ppSeq := range b.postProcessors {
|
||||
for _, corePP := range ppSeq {
|
||||
if err = corePP.processor.Configure(corePP.config); err != nil {
|
||||
err = corePP.processor.Configure(corePP.config, packerConfig)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestBuild_Prepare(t *testing.T) {
|
||||
corePP := build.postProcessors[0][0]
|
||||
pp := corePP.processor.(*TestPostProcessor)
|
||||
assert.True(pp.configCalled, "config should be called")
|
||||
assert.Equal(pp.configVal, 42, "config should have right value")
|
||||
assert.Equal(pp.configVal, []interface{}{42, packerConfig}, "config should have right value")
|
||||
}
|
||||
|
||||
func TestBuild_Prepare_Twice(t *testing.T) {
|
||||
|
||||
@@ -10,13 +10,13 @@ type cmdPostProcessor struct {
|
||||
client *Client
|
||||
}
|
||||
|
||||
func (c *cmdPostProcessor) Configure(config interface{}) error {
|
||||
func (c *cmdPostProcessor) Configure(config ...interface{}) error {
|
||||
defer func() {
|
||||
r := recover()
|
||||
c.checkExit(r, nil)
|
||||
}()
|
||||
|
||||
return c.p.Configure(config)
|
||||
return c.p.Configure(config...)
|
||||
}
|
||||
|
||||
func (c *cmdPostProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.Artifact, bool, error) {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
type helperPostProcessor byte
|
||||
|
||||
func (helperPostProcessor) Configure(interface{}) error {
|
||||
func (helperPostProcessor) Configure(...interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ type PostProcessor interface {
|
||||
// Configure is responsible for setting up configuration, storing
|
||||
// the state for later, and returning and errors, such as validation
|
||||
// errors.
|
||||
Configure(interface{}) error
|
||||
Configure(...interface{}) error
|
||||
|
||||
// PostProcess takes a previously created Artifact and produces another
|
||||
// Artifact. If an error occurs, it should return that error. If `keep`
|
||||
|
||||
@@ -4,13 +4,13 @@ type TestPostProcessor struct {
|
||||
artifactId string
|
||||
keep bool
|
||||
configCalled bool
|
||||
configVal interface{}
|
||||
configVal []interface{}
|
||||
ppCalled bool
|
||||
ppArtifact Artifact
|
||||
ppUi Ui
|
||||
}
|
||||
|
||||
func (pp *TestPostProcessor) Configure(v interface{}) error {
|
||||
func (pp *TestPostProcessor) Configure(v ...interface{}) error {
|
||||
pp.configCalled = true
|
||||
pp.configVal = v
|
||||
return nil
|
||||
|
||||
@@ -153,12 +153,14 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface
|
||||
var cmd packer.RemoteCmd
|
||||
cmd.Command = args.Command
|
||||
|
||||
toClose := make([]net.Conn, 0)
|
||||
if args.StdinAddress != "" {
|
||||
stdinC, err := net.Dial("tcp", args.StdinAddress)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
toClose = append(toClose, stdinC)
|
||||
cmd.Stdin = stdinC
|
||||
}
|
||||
|
||||
@@ -168,6 +170,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface
|
||||
return err
|
||||
}
|
||||
|
||||
toClose = append(toClose, stdoutC)
|
||||
cmd.Stdout = stdoutC
|
||||
}
|
||||
|
||||
@@ -177,6 +180,7 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface
|
||||
return err
|
||||
}
|
||||
|
||||
toClose = append(toClose, stderrC)
|
||||
cmd.Stderr = stderrC
|
||||
}
|
||||
|
||||
@@ -196,6 +200,9 @@ func (c *CommunicatorServer) Start(args *CommunicatorStartArgs, reply *interface
|
||||
// exit. When it does, report it back to caller...
|
||||
go func() {
|
||||
defer responseC.Close()
|
||||
for _, conn := range toClose {
|
||||
defer conn.Close()
|
||||
}
|
||||
|
||||
for !cmd.Exited {
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
|
||||
@@ -17,6 +17,10 @@ type PostProcessorServer struct {
|
||||
p packer.PostProcessor
|
||||
}
|
||||
|
||||
type PostProcessorConfigureArgs struct {
|
||||
Configs []interface{}
|
||||
}
|
||||
|
||||
type PostProcessorProcessResponse struct {
|
||||
Err error
|
||||
Keep bool
|
||||
@@ -26,8 +30,9 @@ type PostProcessorProcessResponse struct {
|
||||
func PostProcessor(client *rpc.Client) *postProcessor {
|
||||
return &postProcessor{client}
|
||||
}
|
||||
func (p *postProcessor) Configure(raw interface{}) (err error) {
|
||||
if cerr := p.client.Call("PostProcessor.Configure", &raw, &err); cerr != nil {
|
||||
func (p *postProcessor) Configure(raw ...interface{}) (err error) {
|
||||
args := &PostProcessorConfigureArgs{Configs: raw}
|
||||
if cerr := p.client.Call("PostProcessor.Configure", args, &err); cerr != nil {
|
||||
err = cerr
|
||||
}
|
||||
|
||||
@@ -60,8 +65,8 @@ func (p *postProcessor) PostProcess(ui packer.Ui, a packer.Artifact) (packer.Art
|
||||
return Artifact(client), response.Keep, nil
|
||||
}
|
||||
|
||||
func (p *PostProcessorServer) Configure(raw *interface{}, reply *error) error {
|
||||
*reply = p.p.Configure(*raw)
|
||||
func (p *PostProcessorServer) Configure(args *PostProcessorConfigureArgs, reply *error) error {
|
||||
*reply = p.p.Configure(args.Configs...)
|
||||
if *reply != nil {
|
||||
*reply = NewBasicError(*reply)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package rpc
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"net/rpc"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
@@ -10,13 +11,13 @@ var testPostProcessorArtifact = new(testArtifact)
|
||||
|
||||
type TestPostProcessor struct {
|
||||
configCalled bool
|
||||
configVal interface{}
|
||||
configVal []interface{}
|
||||
ppCalled bool
|
||||
ppArtifact packer.Artifact
|
||||
ppUi packer.Ui
|
||||
}
|
||||
|
||||
func (pp *TestPostProcessor) Configure(v interface{}) error {
|
||||
func (pp *TestPostProcessor) Configure(v ...interface{}) error {
|
||||
pp.configCalled = true
|
||||
pp.configVal = v
|
||||
return nil
|
||||
@@ -56,7 +57,7 @@ func TestPostProcessorRPC(t *testing.T) {
|
||||
t.Fatal("config should be called")
|
||||
}
|
||||
|
||||
if p.configVal != 42 {
|
||||
if !reflect.DeepEqual(p.configVal, []interface{}{42}) {
|
||||
t.Fatalf("unknown config value: %#v", p.configVal)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
@@ -65,6 +66,29 @@ func ParseTemplate(data []byte) (t *Template, err error) {
|
||||
var rawTpl rawTemplate
|
||||
err = json.Unmarshal(data, &rawTpl)
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
+16
-4
@@ -1,12 +1,14 @@
|
||||
package packer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
@@ -86,19 +88,29 @@ func (u *ColoredUi) colorize(message string, color UiColor, bold bool) string {
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Ask(query string) (string, error) {
|
||||
return u.Ui.Ask(fmt.Sprintf("%s: %s", u.SayPrefix, query))
|
||||
return u.Ui.Ask(u.prefixLines(u.SayPrefix, query))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Say(message string) {
|
||||
u.Ui.Say(fmt.Sprintf("%s: %s", u.SayPrefix, message))
|
||||
u.Ui.Say(u.prefixLines(u.SayPrefix, message))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Message(message string) {
|
||||
u.Ui.Message(fmt.Sprintf("%s: %s", u.MessagePrefix, message))
|
||||
u.Ui.Message(u.prefixLines(u.MessagePrefix, message))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) Error(message string) {
|
||||
u.Ui.Error(fmt.Sprintf("%s: %s", u.SayPrefix, message))
|
||||
u.Ui.Error(u.prefixLines(u.SayPrefix, message))
|
||||
}
|
||||
|
||||
func (u *PrefixedUi) prefixLines(prefix, message string) string {
|
||||
var result bytes.Buffer
|
||||
|
||||
for _, line := range strings.Split(message, "\n") {
|
||||
result.WriteString(fmt.Sprintf("%s: %s\n", prefix, line))
|
||||
}
|
||||
|
||||
return strings.TrimSpace(result.String())
|
||||
}
|
||||
|
||||
func (rw *ReaderWriterUi) Ask(query string) (string, error) {
|
||||
|
||||
@@ -50,6 +50,9 @@ func TestPrefixedUi(t *testing.T) {
|
||||
|
||||
prefixUi.Error("bar")
|
||||
assert.Equal(readWriter(bufferUi), "mitchell: bar\n", "should have prefix")
|
||||
|
||||
prefixUi.Say("foo\nbar")
|
||||
assert.Equal(readWriter(bufferUi), "mitchell: foo\nmitchell: bar\n", "should multiline")
|
||||
}
|
||||
|
||||
func TestColoredUi_ImplUi(t *testing.T) {
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
)
|
||||
|
||||
// The version of packer.
|
||||
const Version = "0.1.3"
|
||||
const Version = "0.1.5"
|
||||
|
||||
// Any pre-release marker for the version. If this is "" (empty string),
|
||||
// then it means that it is a final release. Otherwise, this is the
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer/plugin"
|
||||
"github.com/mitchellh/packer/provisioner/file"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plugin.ServeProvisioner(new(file.Provisioner))
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -14,6 +15,8 @@ import (
|
||||
type AWSBoxConfig struct {
|
||||
OutputPath string `mapstructure:"output"`
|
||||
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
|
||||
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
}
|
||||
|
||||
type AWSVagrantfileTemplate struct {
|
||||
@@ -24,10 +27,12 @@ type AWSBoxPostProcessor struct {
|
||||
config AWSBoxConfig
|
||||
}
|
||||
|
||||
func (p *AWSBoxPostProcessor) Configure(raw interface{}) error {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
func (p *AWSBoxPostProcessor) Configure(raws ...interface{}) error {
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -49,7 +54,8 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact
|
||||
}
|
||||
|
||||
// Compile the output path
|
||||
outputPath, err := ProcessOutputPath(p.config.OutputPath, "aws", artifact)
|
||||
outputPath, err := ProcessOutputPath(p.config.OutputPath,
|
||||
p.config.PackerBuildName, "aws", artifact)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -70,14 +76,17 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact
|
||||
|
||||
vagrantfileContents := defaultAWSVagrantfile
|
||||
if p.config.VagrantfileTemplate != "" {
|
||||
log.Printf("Using vagrantfile template: %s", p.config.VagrantfileTemplate)
|
||||
f, err := os.Open(p.config.VagrantfileTemplate)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error opening vagrantfile template: %s", err)
|
||||
return nil, false, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
contents, err := ioutil.ReadAll(f)
|
||||
if err != nil {
|
||||
err = fmt.Errorf("error reading vagrantfile template: %s", err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
@@ -96,6 +105,7 @@ func (p *AWSBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact
|
||||
|
||||
// Compress the directory to the given output path
|
||||
if err := DirToBox(outputPath, dir); err != nil {
|
||||
err = fmt.Errorf("error creating box: %s", err)
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
|
||||
@@ -19,30 +19,44 @@ var builtins = map[string]string{
|
||||
|
||||
type Config struct {
|
||||
OutputPath string `mapstructure:"output"`
|
||||
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
}
|
||||
|
||||
type PostProcessor struct {
|
||||
config Config
|
||||
premade map[string]packer.PostProcessor
|
||||
config Config
|
||||
premade map[string]packer.PostProcessor
|
||||
rawConfigs []interface{}
|
||||
}
|
||||
|
||||
func (p *PostProcessor) Configure(raw interface{}) error {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
func (p *PostProcessor) Configure(raws ...interface{}) error {
|
||||
// Store the raw configs for usage later
|
||||
p.rawConfigs = raws
|
||||
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ppExtraConfig := make(map[string]interface{})
|
||||
if p.config.OutputPath == "" {
|
||||
p.config.OutputPath = "packer_{{.Provider}}.box"
|
||||
p.config.OutputPath = "packer_{{ .BuildName }}_{{.Provider}}.box"
|
||||
ppExtraConfig["output"] = p.config.OutputPath
|
||||
}
|
||||
|
||||
_, err = template.New("output").Parse(p.config.OutputPath)
|
||||
_, err := template.New("output").Parse(p.config.OutputPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("output invalid template: %s", err)
|
||||
}
|
||||
|
||||
// Store the extra configuration for post-processors
|
||||
p.rawConfigs = append(p.rawConfigs, ppExtraConfig)
|
||||
|
||||
// TODO(mitchellh): Properly handle multiple raw configs
|
||||
var mapConfig map[string]interface{}
|
||||
if err := mapstructure.Decode(raw, &mapConfig); err != nil {
|
||||
if err := mapstructure.Decode(raws[0], &mapConfig); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -54,7 +68,12 @@ func (p *PostProcessor) Configure(raw interface{}) error {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := pp.Configure(raw); err != nil {
|
||||
// Create the proper list of configurations
|
||||
ppConfigs := make([]interface{}, 0, len(p.rawConfigs)+1)
|
||||
copy(ppConfigs, p.rawConfigs)
|
||||
ppConfigs = append(ppConfigs, raw)
|
||||
|
||||
if err := pp.Configure(ppConfigs...); err != nil {
|
||||
errors = append(errors, err)
|
||||
}
|
||||
|
||||
@@ -84,8 +103,7 @@ func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (pac
|
||||
return nil, false, fmt.Errorf("Vagrant box post-processor not found: %s", ppName)
|
||||
}
|
||||
|
||||
config := map[string]string{"output": p.config.OutputPath}
|
||||
if err := pp.Configure(config); err != nil {
|
||||
if err := pp.Configure(p.rawConfigs...); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,14 +17,36 @@ import (
|
||||
// OutputPath variables.
|
||||
type OutputPathTemplate struct {
|
||||
ArtifactId string
|
||||
BuildName string
|
||||
Provider string
|
||||
}
|
||||
|
||||
// Copies a file by copying the contents of the file to another place.
|
||||
func CopyContents(dst, src string) error {
|
||||
srcF, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcF.Close()
|
||||
|
||||
dstF, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer dstF.Close()
|
||||
|
||||
if _, err := io.Copy(dstF, srcF); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DirToBox takes the directory and compresses it into a Vagrant-compatible
|
||||
// box. This function does not perform checks to verify that dir is
|
||||
// actually a proper box. This is an expected precondition.
|
||||
func DirToBox(dst, dir string) error {
|
||||
log.Printf("Turning dir into box: %s", dir)
|
||||
log.Printf("Turning dir into box: %s => %s", dir, dst)
|
||||
dstF, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -39,9 +61,14 @@ func DirToBox(dst, dir string) error {
|
||||
|
||||
// This is the walk func that tars each of the files in the dir
|
||||
tarWalk := func(path string, info os.FileInfo, prevErr error) error {
|
||||
// If there was a prior error, return it
|
||||
if prevErr != nil {
|
||||
return prevErr
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if info.IsDir() {
|
||||
log.Printf("Skiping directory '%s' for box '%s'", path, dst)
|
||||
log.Printf("Skipping directory '%s' for box '%s'", path, dst)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -83,11 +110,12 @@ func DirToBox(dst, dir string) error {
|
||||
|
||||
// ProcessOutputPath takes an output path template and executes it,
|
||||
// replacing variables with their respective values.
|
||||
func ProcessOutputPath(path string, provider string, artifact packer.Artifact) (string, error) {
|
||||
func ProcessOutputPath(path string, buildName string, provider string, artifact packer.Artifact) (string, error) {
|
||||
var buf bytes.Buffer
|
||||
|
||||
tplData := &OutputPathTemplate{
|
||||
ArtifactId: artifact.Id(),
|
||||
BuildName: buildName,
|
||||
Provider: provider,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
@@ -18,6 +17,8 @@ import (
|
||||
type VBoxBoxConfig struct {
|
||||
OutputPath string `mapstructure:"output"`
|
||||
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
|
||||
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
}
|
||||
|
||||
type VBoxVagrantfileTemplate struct {
|
||||
@@ -28,10 +29,12 @@ type VBoxBoxPostProcessor struct {
|
||||
config VBoxBoxConfig
|
||||
}
|
||||
|
||||
func (p *VBoxBoxPostProcessor) Configure(raw interface{}) error {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
func (p *VBoxBoxPostProcessor) Configure(raws ...interface{}) error {
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -46,7 +49,8 @@ func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifac
|
||||
}
|
||||
|
||||
// Compile the output path
|
||||
outputPath, err := ProcessOutputPath(p.config.OutputPath, "virtualbox", artifact)
|
||||
outputPath, err := ProcessOutputPath(p.config.OutputPath,
|
||||
p.config.PackerBuildName, "virtualbox", artifact)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -61,19 +65,9 @@ func (p *VBoxBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifac
|
||||
// Copy all of the original contents into the temporary directory
|
||||
for _, path := range artifact.Files() {
|
||||
ui.Message(fmt.Sprintf("Copying: %s", path))
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(filepath.Join(dir, filepath.Base(path)))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
dstPath := filepath.Join(dir, filepath.Base(path))
|
||||
if err := CopyContents(dstPath, path); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
@@ -179,6 +173,6 @@ func (p *VBoxBoxPostProcessor) renameOVF(dir string) error {
|
||||
|
||||
var defaultVBoxVagrantfile = `
|
||||
Vagrant.configure("2") do |config|
|
||||
config.vm.base_mac = "{{ .BaseMacAddress }}"
|
||||
config.vm.base_mac = "{{ .BaseMacAddress }}"
|
||||
end
|
||||
`
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -14,16 +13,20 @@ import (
|
||||
type VMwareBoxConfig struct {
|
||||
OutputPath string `mapstructure:"output"`
|
||||
VagrantfileTemplate string `mapstructure:"vagrantfile_template"`
|
||||
|
||||
PackerBuildName string `mapstructure:"packer_build_name"`
|
||||
}
|
||||
|
||||
type VMwareBoxPostProcessor struct {
|
||||
config VMwareBoxConfig
|
||||
}
|
||||
|
||||
func (p *VMwareBoxPostProcessor) Configure(raw interface{}) error {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
func (p *VMwareBoxPostProcessor) Configure(raws ...interface{}) error {
|
||||
for _, raw := range raws {
|
||||
err := mapstructure.Decode(raw, &p.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -31,7 +34,8 @@ func (p *VMwareBoxPostProcessor) Configure(raw interface{}) error {
|
||||
|
||||
func (p *VMwareBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
|
||||
// Compile the output path
|
||||
outputPath, err := ProcessOutputPath(p.config.OutputPath, "vmware", artifact)
|
||||
outputPath, err := ProcessOutputPath(p.config.OutputPath,
|
||||
p.config.PackerBuildName, "vmware", artifact)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -46,19 +50,9 @@ func (p *VMwareBoxPostProcessor) PostProcess(ui packer.Ui, artifact packer.Artif
|
||||
// Copy all of the original contents into the temporary directory
|
||||
for _, path := range artifact.Files() {
|
||||
ui.Message(fmt.Sprintf("Copying: %s", path))
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer src.Close()
|
||||
|
||||
dst, err := os.Create(filepath.Join(dir, filepath.Base(path)))
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
if _, err := io.Copy(dst, src); err != nil {
|
||||
dstPath := filepath.Join(dir, filepath.Base(path))
|
||||
if err := CopyContents(dstPath, path); err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"os"
|
||||
)
|
||||
|
||||
type config struct {
|
||||
// The local path of the file to upload.
|
||||
Source string
|
||||
|
||||
// The remote path where the local file will be uploaded to.
|
||||
Destination string
|
||||
}
|
||||
|
||||
type Provisioner struct {
|
||||
config config
|
||||
}
|
||||
|
||||
func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
for _, raw := range raws {
|
||||
if err := mapstructure.Decode(raw, &p.config); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
errs := []error{}
|
||||
|
||||
if _, err := os.Stat(p.config.Source); err != nil {
|
||||
errs = append(errs,
|
||||
fmt.Errorf("Bad source '%s': %s", p.config.Source, err))
|
||||
}
|
||||
|
||||
if p.config.Destination == "" {
|
||||
errs = append(errs, errors.New("Destination must be specified."))
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return &packer.MultiError{errs}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
ui.Say(fmt.Sprintf("Uploading %s => %s", p.config.Source, p.config.Destination))
|
||||
f, err := os.Open(p.config.Source)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return comm.Upload(p.config.Destination, f)
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"github.com/mitchellh/packer/packer"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testConfig() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"destination": "something",
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisioner_Impl(t *testing.T) {
|
||||
var raw interface{}
|
||||
raw = &Provisioner{}
|
||||
if _, ok := raw.(packer.Provisioner); !ok {
|
||||
t.Fatalf("must be a provisioner")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_InvalidSource(t *testing.T) {
|
||||
var p Provisioner
|
||||
config := testConfig()
|
||||
config["source"] = "/this/should/not/exist"
|
||||
|
||||
err := p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatalf("should require existing file")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_ValidSource(t *testing.T) {
|
||||
var p Provisioner
|
||||
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
config := testConfig()
|
||||
config["source"] = tf.Name()
|
||||
|
||||
err = p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should allow valid file: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_EmptyDestination(t *testing.T) {
|
||||
var p Provisioner
|
||||
|
||||
config := testConfig()
|
||||
delete(config, "destination")
|
||||
err := p.Prepare(config)
|
||||
if err == nil {
|
||||
t.Fatalf("should require destination path")
|
||||
}
|
||||
}
|
||||
|
||||
type stubUploadCommunicator struct {
|
||||
dest string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (suc *stubUploadCommunicator) Download(src string, data io.Writer) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (suc *stubUploadCommunicator) Upload(dest string, data io.Reader) error {
|
||||
var err error
|
||||
suc.dest = dest
|
||||
suc.data, err = ioutil.ReadAll(data)
|
||||
return err
|
||||
}
|
||||
|
||||
func (suc *stubUploadCommunicator) Start(cmd *packer.RemoteCmd) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type stubUi struct {
|
||||
sayMessages string
|
||||
}
|
||||
|
||||
func (su *stubUi) Ask(string) (string, error) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (su *stubUi) Error(string) {
|
||||
}
|
||||
|
||||
func (su *stubUi) Message(string) {
|
||||
}
|
||||
|
||||
func (su *stubUi) Say(msg string) {
|
||||
su.sayMessages += msg
|
||||
}
|
||||
|
||||
func TestProvisionerProvision_SendsFile(t *testing.T) {
|
||||
var p Provisioner
|
||||
tf, err := ioutil.TempFile("", "packer")
|
||||
if err != nil {
|
||||
t.Fatalf("error tempfile: %s", err)
|
||||
}
|
||||
defer os.Remove(tf.Name())
|
||||
|
||||
if _, err = tf.Write([]byte("hello")); err != nil {
|
||||
t.Fatalf("error writing tempfile: %s", err)
|
||||
}
|
||||
|
||||
config := map[string]interface{}{
|
||||
"source": tf.Name(),
|
||||
"destination": "something",
|
||||
}
|
||||
|
||||
if err := p.Prepare(config); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
ui := &stubUi{}
|
||||
comm := &stubUploadCommunicator{}
|
||||
err = p.Provision(ui, comm)
|
||||
if err != nil {
|
||||
t.Fatalf("should successfully provision: %s", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(ui.sayMessages, tf.Name()) {
|
||||
t.Fatalf("should print source filename")
|
||||
}
|
||||
|
||||
if !strings.Contains(ui.sayMessages, "something") {
|
||||
t.Fatalf("should print destination filename")
|
||||
}
|
||||
|
||||
if comm.dest != "something" {
|
||||
t.Fatalf("should upload to configured destination")
|
||||
}
|
||||
|
||||
if string(comm.data) != "hello" {
|
||||
t.Fatalf("should upload with source file's data")
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,9 @@ type config struct {
|
||||
// in the context of a single shell.
|
||||
Inline []string
|
||||
|
||||
// The shebang value used when running inline scripts.
|
||||
InlineShebang string `mapstructure:"inline_shebang"`
|
||||
|
||||
// The local path of the shell script to upload and execute.
|
||||
Script string
|
||||
|
||||
@@ -62,13 +65,17 @@ func (p *Provisioner) Prepare(raws ...interface{}) error {
|
||||
}
|
||||
|
||||
if p.config.ExecuteCommand == "" {
|
||||
p.config.ExecuteCommand = "{{.Vars}} sh {{.Path}}"
|
||||
p.config.ExecuteCommand = "chmod +x {{.Path}}; {{.Vars}} {{.Path}}"
|
||||
}
|
||||
|
||||
if p.config.Inline != nil && len(p.config.Inline) == 0 {
|
||||
p.config.Inline = nil
|
||||
}
|
||||
|
||||
if p.config.InlineShebang == "" {
|
||||
p.config.InlineShebang = "/bin/sh"
|
||||
}
|
||||
|
||||
if p.config.RemotePath == "" {
|
||||
p.config.RemotePath = DefaultRemotePath
|
||||
}
|
||||
@@ -136,6 +143,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
|
||||
// Write our contents to it
|
||||
writer := bufio.NewWriter(tf)
|
||||
writer.WriteString(fmt.Sprintf("#!%s\n", p.config.InlineShebang))
|
||||
for _, command := range p.config.Inline {
|
||||
if _, err := writer.WriteString(command + "\n"); err != nil {
|
||||
return fmt.Errorf("Error preparing shell script: %s", err)
|
||||
@@ -157,6 +165,7 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
if err != nil {
|
||||
return fmt.Errorf("Error opening shell script: %s", err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
log.Printf("Uploading %s => %s", path, p.config.RemotePath)
|
||||
err = comm.Upload(p.config.RemotePath, f)
|
||||
@@ -164,6 +173,9 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
|
||||
return fmt.Errorf("Error uploading shell script: %s", err)
|
||||
}
|
||||
|
||||
// Close the original file since we copied it
|
||||
f.Close()
|
||||
|
||||
// Flatten the environment variables
|
||||
flattendVars := strings.Join(p.config.Vars, " ")
|
||||
|
||||
|
||||
@@ -35,6 +35,33 @@ func TestProvisionerPrepare_Defaults(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_InlineShebang(t *testing.T) {
|
||||
config := testConfig()
|
||||
|
||||
delete(config, "inline_shebang")
|
||||
p := new(Provisioner)
|
||||
err := p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if p.config.InlineShebang != "/bin/sh" {
|
||||
t.Fatalf("bad value: %s", p.config.InlineShebang)
|
||||
}
|
||||
|
||||
// Test with a good one
|
||||
config["inline_shebang"] = "foo"
|
||||
p = new(Provisioner)
|
||||
err = p.Prepare(config)
|
||||
if err != nil {
|
||||
t.Fatalf("should not have error: %s", err)
|
||||
}
|
||||
|
||||
if p.config.InlineShebang != "foo" {
|
||||
t.Fatalf("bad value: %s", p.config.InlineShebang)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvisionerPrepare_Script(t *testing.T) {
|
||||
config := testConfig()
|
||||
delete(config, "inline")
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
ruby '1.9.3'
|
||||
|
||||
gem "middleman", "~> 3.0.6"
|
||||
gem "middleman-minify-html", "~> 3.0.0"
|
||||
gem "rack-contrib", "~> 1.1.0"
|
||||
|
||||
+1
-1
@@ -17,7 +17,7 @@ commands:
|
||||
|
||||
```
|
||||
$ bundle
|
||||
$ bundle exec middleman server
|
||||
$ PACKER_DISABLE_DOWNLOAD_FETCH=true PACKER_VERSION=1.0 bundle exec middleman server
|
||||
```
|
||||
|
||||
Then open up `localhost:4567`. Note that some URLs you may need to append
|
||||
|
||||
+1
-1
@@ -1,6 +1,5 @@
|
||||
require "net/http"
|
||||
|
||||
raise "BINTRAY_API_KEY must be set." if !ENV["BINTRAY_API_KEY"]
|
||||
raise "PACKER_VERSION must be set." if !ENV["PACKER_VERSION"]
|
||||
|
||||
#-------------------------------------------------------------------------
|
||||
@@ -11,6 +10,7 @@ $packer_files = {}
|
||||
$packer_os = []
|
||||
|
||||
if !ENV["PACKER_DISABLE_DOWNLOAD_FETCH"]
|
||||
raise "BINTRAY_API_KEY must be set." if !ENV["BINTRAY_API_KEY"]
|
||||
http = Net::HTTP.new("dl.bintray.com", 80)
|
||||
req = Net::HTTP::Get.new("/mitchellh/packer")
|
||||
req.basic_auth "mitchellh", ENV["BINTRAY_API_KEY"]
|
||||
|
||||
@@ -4,7 +4,7 @@ layout: "docs"
|
||||
|
||||
# Packer Terminology
|
||||
|
||||
There are a handful of terms used throughout Packer documentation where
|
||||
There are a handful of terms used throughout the Packer documentation where
|
||||
the meaning may not be immediately obvious if you haven't used Packer before.
|
||||
Luckily, there are relatively few. This page documents all the terminology
|
||||
required to understand and use Packer. The terminology is in alphabetical
|
||||
|
||||
@@ -37,6 +37,8 @@ each category, the available configuration keys are alphabetized.
|
||||
Required:
|
||||
|
||||
* `access_key` (string) - The access key used to communicate with AWS.
|
||||
If not specified, Packer will attempt to read this from environmental
|
||||
variables `AWS_ACCESS_KEY_ID` or `AWS_ACCESS_KEY` (in that order).
|
||||
|
||||
* `ami_name` (string) - The name of the resulting AMI that will appear
|
||||
when managing AMIs in the AWS console or via APIs. This must be unique.
|
||||
@@ -50,6 +52,8 @@ Required:
|
||||
to launch the EC2 instance to create the AMI.
|
||||
|
||||
* `secret_key` (string) - The secret key used to communicate with AWS.
|
||||
If not specified, Packer will attempt to read this from environmental
|
||||
variables `AWS_SECRET_ACCESS_KEY` or `AWS_SECRET_KEY` (in that order).
|
||||
|
||||
* `source_ami` (string) - The initial AMI used as a base for the newly
|
||||
created machine.
|
||||
@@ -77,12 +81,19 @@ Here is a basic example. It is completely valid except for the access keys:
|
||||
"secret_key": "YOUR SECRET KEY HERE",
|
||||
"region": "us-east-1",
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "m1.small",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer-quick-start {{.CreateTime}}"
|
||||
}
|
||||
</pre>
|
||||
|
||||
<div class="alert alert-block alert-info">
|
||||
<strong>Note:</strong> Packer can also read the access key and secret
|
||||
access key from environmental variables. See the configuration reference in
|
||||
the section above for more information on what environmental variables Packer
|
||||
will look for.
|
||||
</div>
|
||||
|
||||
## AMI Name Variables
|
||||
|
||||
The AMI name specified by the `ami_name` configuration variable is actually
|
||||
|
||||
@@ -89,7 +89,7 @@ the prior linked page for information on syntax if you're unfamiliar with it.
|
||||
|
||||
The available variables are shown below:
|
||||
|
||||
* `CreateTime`- This will be replaced with the Unix timestamp of when the
|
||||
* `CreateTime` - This will be replaced with the Unix timestamp of when the
|
||||
image is created.
|
||||
|
||||
## Finding Image, Region, and Size IDs
|
||||
|
||||
@@ -27,7 +27,8 @@ Ubuntu to self-install. Still, the example serves to show the basic configuratio
|
||||
"iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.2-server-amd64.iso",
|
||||
"iso_md5": "af5f788aee1b32c4b2634734309cc9e9",
|
||||
"ssh_username": "packer",
|
||||
"ssh_wait_timeout": "30s"
|
||||
"ssh_wait_timeout": "30s",
|
||||
"shutdown_command": "shutdown -P now"
|
||||
}
|
||||
</pre>
|
||||
|
||||
@@ -54,7 +55,7 @@ Required:
|
||||
Optional:
|
||||
|
||||
* `boot_command` (array of strings) - This is an array of commands to type
|
||||
when the virtual machine is firsted booted. The goal of these commands should
|
||||
when the virtual machine is first booted. The goal of these commands should
|
||||
be to type just enough to initialize the operating system installer. Special
|
||||
keys can be typed as well, and are covered in the section below on the boot
|
||||
command. If this is not specified, it is assumed the installer will start
|
||||
@@ -75,6 +76,16 @@ Optional:
|
||||
of the user. This is a [configuration template](/docs/templates/configuration-templates.html)
|
||||
where the `Version` variable is replaced with the VirtualBox version.
|
||||
|
||||
* `guest_additions_sha256` (string) - The SHA256 checksum of the guest
|
||||
additions ISO that will be uploaded to the guest VM. By default the
|
||||
checksums will be downloaded from the VirtualBox website, so this only
|
||||
needs to be set if you want to be explicit about the checksum.
|
||||
|
||||
* `guest_additions_url` (string) - The URL to the guest additions ISO
|
||||
to upload. This can also be a file URL if the ISO is at a local path.
|
||||
By default the VirtualBox builder will go and download the proper
|
||||
guest additions ISO from the internet.
|
||||
|
||||
* `guest_os_type` (string) - The guest OS type being installed. By default
|
||||
this is "other", but you can get _dramatic_ performance improvements by
|
||||
setting this to the proper value. To view all available values for this
|
||||
@@ -82,6 +93,11 @@ Optional:
|
||||
how to optimize the virtual hardware to work best with that operating
|
||||
system.
|
||||
|
||||
* `headless` (bool) - Packer defaults to building VirtualBox
|
||||
virtual machines by launching a GUI that shows the console of the
|
||||
machine being built. When this value is set to true, the machine will
|
||||
start without a console.
|
||||
|
||||
* `http_directory` (string) - Path to a directory to serve using an HTTP
|
||||
server. The files in this directory will be available over HTTP that will
|
||||
be requestable from the virtual machine. This is useful for hosting
|
||||
@@ -127,7 +143,7 @@ Optional:
|
||||
|
||||
* `ssh_wait_timeout` (string) - The duration to wait for SSH to become
|
||||
available. By default this is "20m", or 20 minutes. Note that this should
|
||||
be quite long since the timer begins as soon as virtual machine is booted.
|
||||
be quite long since the timer begins as soon as the virtual machine is booted.
|
||||
|
||||
* `vboxmanage` (array of array of strings) - Custom `VBoxManage` commands to
|
||||
execute in order to further customize the virtual machine being created.
|
||||
@@ -171,7 +187,7 @@ will be replaced by the proper key:
|
||||
|
||||
* `<tab>` - Simulates pressing the tab key.
|
||||
|
||||
* `<wait>` - Adds a one second pause before sending any additional keys. This
|
||||
* `<wait>` `<wait5>` `<wait10>` - Adds a 1, 5 or 10 second pause before sending any additional keys. This
|
||||
is useful if you have to generally wait for the UI to update before typing more.
|
||||
|
||||
In addition to the special keys, each command to type is treated as a
|
||||
|
||||
@@ -28,7 +28,8 @@ Ubuntu to self-install. Still, the example serves to show the basic configuratio
|
||||
"iso_url": "http://releases.ubuntu.com/12.04/ubuntu-12.04.2-server-amd64.iso",
|
||||
"iso_md5": "af5f788aee1b32c4b2634734309cc9e9",
|
||||
"ssh_username": "packer",
|
||||
"ssh_wait_timeout": "30s"
|
||||
"ssh_wait_timeout": "30s",
|
||||
"shutdown_command": "shutdown -P now"
|
||||
}
|
||||
</pre>
|
||||
|
||||
@@ -77,6 +78,13 @@ Optional:
|
||||
OS type, VMware may perform some optimizations or virtual hardware changes
|
||||
to better support the operating system running in the virtual machine.
|
||||
|
||||
* `headless` (bool) - Packer defaults to building VMware
|
||||
virtual machines by launching a GUI that shows the console of the
|
||||
machine being built. When this value is set to true, the machine will
|
||||
start without a console. For VMware machines, Packer will output VNC
|
||||
connection information in case you need to connect to the console to
|
||||
debug the build process.
|
||||
|
||||
* `http_directory` (string) - Path to a directory to serve using an HTTP
|
||||
server. The files in this directory will be available over HTTP that will
|
||||
be requestable from the virtual machine. This is useful for hosting
|
||||
@@ -99,6 +107,12 @@ Optional:
|
||||
By default this is "output-BUILDNAME" where "BUILDNAME" is the name
|
||||
of the build.
|
||||
|
||||
* `skip_compaction` (bool) - VMware-created disks are defragmented
|
||||
and compacted at the end of the build process using `vmware-vdiskmanager`.
|
||||
In certain rare cases, this might actually end up making the resulting disks
|
||||
slightly larger. If you find this to be the case, you can disable compaction
|
||||
using this configuration value.
|
||||
|
||||
* `shutdown_command` (string) - The command to use to gracefully shut down
|
||||
the machine once all the provisioning is done. By default this is an empty
|
||||
string, which tells Packer to just forcefully shut down the machine.
|
||||
@@ -116,7 +130,7 @@ Optional:
|
||||
|
||||
* `ssh_wait_timeout` (string) - The duration to wait for SSH to become
|
||||
available. By default this is "20m", or 20 minutes. Note that this should
|
||||
be quite long since the timer begins as soon as virtual machine is booted.
|
||||
be quite long since the timer begins as soon as the virtual machine is booted.
|
||||
|
||||
* `tools_upload_flavor` (string) - The flavor of the VMware Tools ISO to
|
||||
upload into the VM. Valid values are "darwin", "linux", and "windows".
|
||||
@@ -134,7 +148,7 @@ Optional:
|
||||
where "BUILDNAME" is the name of the build.
|
||||
|
||||
* `vmdk_name` (string) - The filename of the virtual disk that'll be created,
|
||||
without the extension. This is a This defaults to "packer".
|
||||
without the extension. This defaults to "packer".
|
||||
|
||||
* `vmx_data` (object, string keys and string values) - Arbitrary key/values
|
||||
to enter into the virtual machine VMX file. This is for advanced users
|
||||
@@ -168,7 +182,7 @@ will be replaced by the proper key:
|
||||
|
||||
* `<tab>` - Simulates pressing the tab key.
|
||||
|
||||
* `<wait>` - Adds a one second pause before sending any additional keys. This
|
||||
* `<wait>` `<wait5>` `<wait10>` - Adds a 1, 5 or 10 second pause before sending any additional keys. This
|
||||
is useful if you have to generally wait for the UI to update before typing more.
|
||||
|
||||
In addition to the special keys, each command to type is treated as a
|
||||
|
||||
@@ -14,7 +14,7 @@ artifacts that are created will be outputted at the end of the build.
|
||||
* `-debug` - Disables parallelization and enables debug mode. Debug mode flags
|
||||
the builders that they should output debugging information. The exact behavior
|
||||
of debug mode is left to the builder. In general, builders usually will stop
|
||||
between each step, waiting keyboard input before continuing. This will allow
|
||||
between each step, waiting for keyboard input before continuing. This will allow
|
||||
the user to inspect state and so on.
|
||||
|
||||
* `-except=foo,bar,baz` - Builds all the builds except those with the given
|
||||
|
||||
@@ -52,7 +52,7 @@ it is not required. You're allowed to make the help look like anything
|
||||
you please.
|
||||
|
||||
```
|
||||
Usage: packer COMMAND ARGS...
|
||||
Usage: packer COMMAND [options] ARGS...
|
||||
|
||||
Brief one or two sentence about the function of the command.
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ Plugins allow new functionality to be added to Packer without
|
||||
modifying the core source code. Packer plugins are able to add new
|
||||
commands, builders, provisioners, hooks, and more. In fact, much of Packer
|
||||
itself is implemented by writing plugins that are simply distributed with
|
||||
the Packer. For example, all the commands, builders, provisioners, and more
|
||||
Packer. For example, all the commands, builders, provisioners, and more
|
||||
that ship with Packer are implemented as Plugins that are simply hardcoded
|
||||
to load with Packer.
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ for your system and download it. Packer is packaged as a "zip" file.
|
||||
Next, unzip the downloaded package into a directory where Packer will be
|
||||
installed. On Unix systems, `~/packer` or `/usr/local/packer` is generally good,
|
||||
depending on whether you want to restrict the install to just your user
|
||||
or install it system-wide. On Windows systems, you can put it whereever you'd
|
||||
or install it system-wide. On Windows systems, you can put it wherever you'd
|
||||
like.
|
||||
|
||||
After unzipping the package, the directory should contain a set of binary
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
layout: "docs"
|
||||
---
|
||||
|
||||
# File Provisioner
|
||||
|
||||
Type: `file`
|
||||
|
||||
The file provisioner uploads files to machines built by Packer. The
|
||||
recommended usage of the file provisioner is to use it to upload files,
|
||||
and then use [shell provisioner](/docs/provisioners/shell.html) to move
|
||||
them to the proper place, set permissions, etc.
|
||||
|
||||
## Basic Example
|
||||
|
||||
<pre class="prettyprint">
|
||||
{
|
||||
"type": "file",
|
||||
"source": "app.tar.gz",
|
||||
"destination": "/tmp/app.tar.gz"
|
||||
}
|
||||
</pre>
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
The available configuration options are listed below. All elements are required.
|
||||
|
||||
* `source` (string) - The path to a local file to upload to the machine. The
|
||||
path can be absolute or relative. If it is relative, it is relative to the
|
||||
working directory when Packer is executed.
|
||||
|
||||
* `destination` (string) - The path where the file will be uploaded to in the
|
||||
machine. This value must be a writable location and any parent directories
|
||||
must already exist.
|
||||
@@ -23,7 +23,7 @@ The example below is fully functional.
|
||||
|
||||
## Configuration Reference
|
||||
|
||||
The reference of available configuratin options is listed below. The only
|
||||
The reference of available configuration options is listed below. The only
|
||||
required element is either "inline" or "script". Every other option is optional.
|
||||
|
||||
Exactly _one_ of the following is required:
|
||||
@@ -51,12 +51,17 @@ Optional parameters:
|
||||
`key=value`.
|
||||
|
||||
* `execute_command` (string) - The command to use to execute the script.
|
||||
By default this is `{{ .Vars }} sh {{ .Path }}`. The value of this is
|
||||
By default this is `chmod +x {{ .Path }}; {{ .Vars }} {{ .Path }}`. The value of this is
|
||||
treated as [configuration template](/docs/templates/configuration-
|
||||
templates.html). There are two available variables: `Path`, which is
|
||||
the path to the script to run, and `Vars`, which is the list of
|
||||
`environment_vars`, if configured.
|
||||
|
||||
* `inline_shebang` (string) - The
|
||||
[shebang](http://en.wikipedia.org/wiki/Shebang_(Unix)) value to use when
|
||||
running commands specified by `inline`. By default, this is `/bin/sh`.
|
||||
If you're not using `inline`, then this configuration has no effect.
|
||||
|
||||
* `remote_path` (string) - The path where the script will be uploaded to
|
||||
in the machine. This defaults to "/tmp/script.sh". This value must be
|
||||
a writable location and any parent directories must already exist.
|
||||
|
||||
@@ -54,6 +54,7 @@ missing valid AWS access keys. Otherwise, it would work properly with
|
||||
"secret_key": "...",
|
||||
"region": "us-east-1",
|
||||
"source_ami": "ami-de0d9eb7",
|
||||
"instance_type": "t1.micro",
|
||||
"ssh_username": "ubuntu",
|
||||
"ami_name": "packer {{.CreateTime}}"
|
||||
}
|
||||
@@ -62,7 +63,7 @@ missing valid AWS access keys. Otherwise, it would work properly with
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "shell",
|
||||
"path": "setup_things.sh"
|
||||
"script": "setup_things.sh"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ A **sequence definition** is a JSON array comprised of other **simple** or
|
||||
**detailed** definitions. The post-processors defined in the array are run
|
||||
in order, with the artifact of each feeding into the next, and any intermediary
|
||||
artifacts being discarded. A sequence definition may not contain another
|
||||
sequence definition. Sequnce definitions are used to chain together multiple
|
||||
sequence definition. Sequence definitions are used to chain together multiple
|
||||
post-processors. An example is shown below, where the artifact of a build is
|
||||
compressed then uploaded, but the compressed result is not kept.
|
||||
|
||||
@@ -93,7 +93,7 @@ are simply shortcuts for a **sequence** definition of only one element.
|
||||
## Input Artifacts
|
||||
|
||||
When using post-processors, the input artifact (coming from a builder or
|
||||
another post-proccessor) is discarded by default after the post-processor runs.
|
||||
another post-processor) is discarded by default after the post-processor runs.
|
||||
This is because generally, you don't want the intermediary artifacts on the
|
||||
way to the final artifact created.
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ the `type` key. This key specifies the name of the provisioner to use.
|
||||
Additional keys within the object are used to configure the provisioner,
|
||||
with the exception of a handful of special keys, covered later.
|
||||
|
||||
As an example, the "shell" provisioner requires at least the `script` key,
|
||||
As an example, the "shell" provisioner requires a key such as `script`
|
||||
which specifies a path to a shell script to execute within the machines
|
||||
being created.
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ specific builder configurations.
|
||||
|
||||
None, really. The tool will tell you if it can't convert a part of a
|
||||
template, and whether that is a critical error or just a warning.
|
||||
Most of Veewees functions translate perfectly over to Packer. There are
|
||||
Most of Veewee's functions translate perfectly over to Packer. There are
|
||||
still a couple missing features in Packer, but they're minimal.
|
||||
|
||||
## Bugs
|
||||
|
||||
@@ -73,7 +73,7 @@ The entire template should now [look like this](https://gist.github.com/mitchell
|
||||
|
||||
Additional builders are simply added to the `builders` array in the template.
|
||||
This tells Packer to build multiple images. The builder `type` values don't
|
||||
even need tdo be different! In fact, if you wanted to build multiple AMIs,
|
||||
even need to be different! In fact, if you wanted to build multiple AMIs,
|
||||
you can do that as well.
|
||||
|
||||
Validate the template with `packer validate`. This is always a good practice.
|
||||
|
||||
@@ -9,7 +9,7 @@ next_title: "Why Use Packer?"
|
||||
# Introduction to Packer
|
||||
|
||||
Welcome to the world of Packer! This introduction guide will show you what
|
||||
packer is, explain why it exists, the benefits it has to offer, and how
|
||||
Packer is, explain why it exists, the benefits it has to offer, and how
|
||||
you can get started with it. If you're already familiar with Packer, the
|
||||
[documentation](/docs) provides more of a reference for all available features.
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@
|
||||
<ul>
|
||||
<li><h4>Provisioners</h4></li>
|
||||
<li><a href="/docs/provisioners/shell.html">Shell Scripts</a></li>
|
||||
<li><a href="/docs/provisioners/file.html">File Uploads</a></li>
|
||||
<li><a href="/docs/provisioners/custom.html">Custom</a></li>
|
||||
</ul>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user